Ruby / Editorial solution guide
Diagnose lost return values and masked failures
Trace last expressions, callback return targets, rescue scope, and ensure behavior before replacing failures with defaults.
Read-only knowledge base · No public comments
Recognize the symptoms
A fictional archive count helper begins returning nil after logging is added. Another import appears successful even when conversion fails, and an explicit return from ensure hides an earlier error. These are control-flow and result-contract problems. Repeating the call or adding a broad rescue can conceal the cause rather than restore the promised outcome.
Understand the likely causes
- Ruby methods return their final evaluated expression unless control flow returns elsewhere. puts commonly returns nil. Ordinary Proc return can leave its defining method, broad rescue can collapse failures into plausible values, and explicit ensure return or failure can mask what came before. Identify which mechanism actually explains the observed path before changing the interface.
Follow the value through each expression boundary
Start with the method’s promised result and list the expressions that can become its final value in each branch. Include rescue, optional blocks, and early returns. A printed correct count does not prove the method returned it. The fixture below deliberately prints a marker last and then observes a nil result. It is a diagnostic model only, not a real incident transcript. Avoid adding another print as a fix before deciding whether the method should return data, observe a value, or transform a callback result.
def count_records(records)
records.length
puts 'counted'
end
result = count_records([1, 2])
puts result.nil?
counted
true
Expected output is counted then true. The length was computed but was not the final returned expression. No Ruby interpreter executed the source.
Distinguish local result loss from non-local control flow
If a callback contains return, determine whether it is a lambda or an ordinary Proc and where it was created. A non-local return can skip the expression a wrapper expected to execute, or fail after its target method ends. If a bang transformation returns nil when unchanged, the fix concerns that API’s return contract instead. Do not merge these mechanisms merely because the caller eventually sees nil or an exception. Narrow the reproduction to one path and preserve a synthetic input that distinguishes the competing explanations.
Preserve the primary result or failure through ensure
A normal ensure body does not replace the protected body’s result, but an explicit return or a newly raised error can do so. The corrected helper below leaves the count as the body result and uses ensure only for an observational marker. Its invalid input still raises ArgumentError for the outer handler. This does not prove real cleanup succeeded; no resource exists here. A real cleanup policy should retain the primary cause while making secondary closure failures visible rather than silently converting everything into success.
def checked_count(records)
raise ArgumentError, 'array required' unless records.is_a?(Array)
records.length
ensure
puts 'cleanup observed'
end
puts checked_count([1, 2])
begin
checked_count(nil)
rescue ArgumentError
puts 'rejected'
end
cleanup observed
2
cleanup observed
rejected
Expected lines are cleanup observed, 2, cleanup observed, rejected. These are reasoned results from unexecuted source, not a verified resource-cleanup test.
Keep rescue as narrow as the meaning it translates
A broad rescue around an entire import can turn storage failure, programmer misuse, and invalid external data into the same empty result. Map each declared exception to a truthful application outcome and let unexpected failures retain a distinct path. Bare raise inside rescue re-raises the current exception; translating to another exception should preserve useful cause context. In an isolated compatible interpreter, test successful return, expected rejection, unexpected failure, callback return behavior, and cleanup failure independently. This guide performs no interpreter run, external operation, rollback, or production recovery.
Verify the fix
The original caller must receive the intended data on success and the declared failure on rejection, regardless of added diagnostics. Check state and side effects as well as returned values. Capture actual engine, version, command, and results when available; the examples here remain unexecuted.
Keep it from recurring
- Review the final expression whenever adding logging, wrapping methods, changing blocks, or editing ensure. Keep observer helpers distinct from transformations, avoid rescue-to-default shortcuts without a product contract, and test results rather than only whether code printed a reassuring marker.