Ruby / Contracts: callables, evaluation, failures, and value models / Lesson 9
Translate exceptions without hiding failures
Use narrow rescue clauses, explicit rejection, preserved causes, and ensure cleanup while keeping successful data distinct from fallback.
On this page
Lesson goals
- Explain the StandardError boundary of an ordinary rescue and why broad rescue Exception is usually misleading.
- Preserve the intended result or failure through cleanup and error translation.
Expected rejection and a defect need different handling
Malformed external input can be an ordinary validation result, while a violated internal assumption may indicate caller misuse or a programming defect. Choose that contract before selecting rescue syntax. Ruby methods do not declare checked exception lists, so important failure behavior needs documentation and targeted tests. An unqualified rescue handles StandardError descendants, not every Exception descendant. Catching Exception broadly can intercept process-level signals such as interruption or exit that the application should not casually turn into empty success. Handle the failures the boundary understands and preserve the rest.
Catch the conversion at the conversion boundary
Integer on invalid text raises ArgumentError. The example confines that operation to a small begin/rescue and prints a fixed rejection message. A large protected block containing conversion, storage, and rendering could misclassify another ArgumentError as malformed text. Rescue modifiers such as expression rescue nil can hide that same ambiguity in a short expression. Prefer explicit boundaries when a failure has domain meaning. The ensure marker below demonstrates ordinary stack-unwinding behavior only; no real resource is acquired and no Ruby interpreter executed the script.
begin
value = Integer('bad', 10)
puts value
rescue ArgumentError
puts 'invalid quantity'
ensure
puts 'conversion finished'
end
invalid quantity
conversion finished
Expected output is invalid quantity then conversion finished. No valid converted value is printed. This source is unexecuted and does not close a real resource.
Keep the original diagnostic meaning
Within rescue, bare raise re-raises the current exception. When translating to an application-specific StandardError subclass, retain the useful cause and safe context instead of inventing an unrelated success. Ruby supports exception cause chains; inspect them rather than discarding the original failure. Avoid placing credentials, personal records, or full unbounded input into messages and logs. A result such as not found, invalid, unavailable, or failed must reflect the actual evidence. Catching an exception proves only that a handler ran, not that partial effects were undone or a retry is safe.
Ensure normally preserves the body result
An ensure body runs as control leaves its protected scope under ordinary exception handling. Its normal final expression does not replace the body’s return value, but an explicit return or a new exception from ensure can mask an earlier result or failure. The helper below returns the yielded number while printing a cleanup marker. This is a control-flow model, not resource verification. Real cleanup may itself fail and needs an owner and a policy for preserving the important primary failure. Abrupt process termination is not a promise that every ensure action can complete.
def with_notice
value = yield
value
ensure
puts 'cleanup marker'
end
puts with_notice { 7 }
cleanup marker
7
Expected output is cleanup marker then 7. The ensure print does not replace the body’s result. No file, service, or interpreter was involved.
Verify state after failure instead of merely counting rescues
A useful failure check inspects the caller’s outcome, relevant invariant, owned resources, and any completed side effects. A retry must follow the operation’s identity and repeat-safety contract, especially if a provider may have committed before reporting an error. Keep expected validation separate from operational outage and internal defects. In an isolated project, exercise success, declared rejection, unexpected failure, and cleanup failure. Record the interpreter, inputs, and actual results. This course describes the plan and reasoned examples only; it performed no Ruby exception, rollback, or recovery execution.
Common errors and fixes
An outage becomes an empty successful result.
- Why it happens
- Broad rescue collapsed different failure meanings into one fallback.
- How to fix it
- Translate only understood failures and preserve explicit unavailable or failed outcomes.
- Prevent it next time
- Define each fallback’s business meaning before implementing it.
An ensure return hides the original exception.
- Why it happens
- Cleanup changed the protected control-flow result.
- How to fix it
- Remove the return and keep cleanup bounded and observable.
- Prevent it next time
- Review cleanup under both success and failure.
A global rescue Exception prevents intended exit behavior.
- Why it happens
- The handler crossed beyond ordinary application StandardError failures.
- How to fix it
- Narrow the exception class and scope to the handled contract.
- Prevent it next time
- Treat interruption and process lifecycle separately from input validation.
Check your understanding
1. Why is begin around a large import followed by rescue ArgumentError ambiguous?
Show worked solution
Many operations inside the import may raise that class for different reasons, including internal misuse. The handler can wrongly blame input conversion. Narrow the protected conversion or translate failures at the layer that understands them, then preserve a distinct path for unexpected defects. Class matching alone is not a root-cause diagnosis.
2. Does an ensure marker prove a failed external write was rolled back?
Show worked solution
No. It shows a local cleanup path reached that statement. The external effect may have completed or partially completed, and cleanup can have its own failure. Verify authoritative state and the operation’s recovery contract before retrying. A local handler cannot manufacture a distributed rollback guarantee.
Key takeaways
- Exception class, protected scope, and fallback meaning must agree.
- Cleanup should preserve truthful results and useful failure causes.