In Ruby, what does the rescue modifier in value = expr rescue fallback catch, and why do style guides discourage it?
answer
- one-line, no class list
- StandardError only
- binds tighter than =
- typos become fallbacks
- Style/RescueModifier
basics
~20 sIt evaluates expr and, if any StandardError is raised, uses fallback instead; since the modifier binds tighter than =, value gets the fallback. It cannot name a class, so it also hides bugs such as NoMethodError from a typo.
solid answer
~40 s`expr rescue fallback` is a statement-level shorthand: if `expr` raises a `StandardError`, the whole statement evaluates to `fallback`. In `value = expr rescue fallback`, the modifier has higher precedence than `=`, so it parses as `value = (expr rescue fallback)` and `value` receives the fallback. The catch is that you cannot restrict the class: a `NoMethodError` from a misspelled method, a `KeyError`, or a `TypeError` from `nil` all silently become the fallback. That is why RuboCop's `Style/RescueModifier` is enabled by default. Prefer a method that reports failure without raising, such as `Integer(str, exception: false)` or `hash.fetch(key, default)`, or a `begin` block that rescues a named class.
code
ruby · 7 linesparams = { "qty" => "abc" }
qty = Integer(params["qty"]) rescue 1 # ArgumentError hidden: qty is 1
qty = Integer(params["qtty"]) rescue 1 # TypeError from Integer(nil) hidden too
qty = Integr(params["qty"]) rescue 1 # NoMethodError from the typo hidden as well
qty = Integer(params.fetch("qty", "1"), exception: false) || 1 # explicit, no rescuego deeper
Know what the one-line form does, that value gets the fallback, and that it cannot name a class.
Explain the precedence rows, especially a if b rescue c, and why the form hides typos and nil errors.
Replace modifiers with non-raising APIs such as Integer(exception: false) and fetch defaults, and keep Style/RescueModifier on in CI.
Frame one-line silent rescues as an observability cost and set a lint policy that allows them only in scripts and consoles.
## What the modifier is The **rescue modifier** puts `rescue` after an expression on one line: `expr rescue fallback`. It behaves like a `begin` block with a bare `rescue`: - `expr` is evaluated; - if it raises a **`StandardError`**, `fallback` is evaluated and becomes the value; - if it raises something outside `StandardError`, the exception propagates as usual. There is **no way to name a class** and no `=> e` binding, so the handler cannot tell which error happened. ## Precedence Ruby's precedence table lists **modifier-rescue** just above assignment, which is why the common form works as expected: | Code | Parsed as | |---|---| | `value = expr rescue fallback` | `value = (expr rescue fallback)` | | `a if b rescue c` | `(a if b) rescue c` | | `stmt if v = expr rescue x` | `stmt if v = (expr rescue x)` | The second row surprises people: the control-expressions documentation explains that `b rescue c` is a statement, not an expression, so it cannot be the condition of the `if` modifier, and the whole line is wrapped instead. The same statement-versus-expression rule means a modifier rescue used inside a method call's argument list must be wrapped in its own parentheses to become an expression. ## Why it is discouraged A broad, silent rescue on a single line hides more than the author intended: 1. **Typos become fallbacks.** `price = item.prise rescue 0` never reports the misspelled method; the `NoMethodError` becomes `0`. 2. **Nil slips through.** `Integer(params[:qty]) rescue 1` turns both a bad string (`ArgumentError`) and a missing parameter (`TypeError` from `Integer(nil)`) into `1`, whether or not that was meant. 3. **No record.** There is no variable to log, so failures leave no trace. 4. **Hard to extend.** Adding logging or narrowing the class means rewriting it as a `begin` block anyway. RuboCop's **`Style/RescueModifier`** is enabled by default, and Standard keeps it on, so most projects already reject the form in CI. ## Better alternatives Many of the cases where the modifier appears have a non-raising API: - **`Integer(str, exception: false)`** and **`Float(str, exception: false)`** return `nil` instead of raising. - **`hash.fetch(key, default)`** or `hash.fetch(key) { ... }` supply a default for a missing key without rescuing `KeyError`. - **`dig`** walks nested hashes and arrays and returns `nil` on a missing level. - When a real exception must be handled, write a `begin` block (or method-level rescue) that names the class and binds `=> e` for logging. ## When it is tolerable In a throwaway script or an `irb` session, `data = JSON.parse(text) rescue {}` is quick and harmless. In application code, the cost of a hidden defect outweighs the saved lines. In a feature-flag lookup, for example, `enabled = flags.fetch("beta") rescue false` hides a misspelled `flags` variable just as happily as a missing key; `flags.fetch("beta", false)` expresses the intent and still fails loudly on the typo. ## Quick reference - **Scope**: `StandardError` and its subclasses, like a bare `rescue`. - **Class list**: none; write a `begin` block to name one. - **Binding**: none; there is no `=> e`, so the fallback cannot log the error by name. - **Precedence**: above `=`, below the ternary `?:`. - **Lint**: `Style/RescueModifier`, enabled by default. ## Reading legacy code Older Ruby code uses the modifier heavily, often as `x = foo rescue nil`. When touching such a line, ask which failure the author expected, then replace the modifier with the matching non-raising API or a named rescue. Tests that exercised only the happy path rarely cover the swallowed error, so add one that forces the failure and asserts the new, explicit behaviour.
- How does `a if b rescue c` parse, and why?`a if b rescue c` parses as `(a if b) rescue c`. A modifier rescue forms a statement, not an expression, and the condition of an `if` modifier must be an expression, so Ruby applies the rescue to the whole `if` statement. Adding parentheses, `a if (b rescue c)`, gives the other reading.
- Does the rescue modifier catch an Interrupt or a LoadError?No. It has the same scope as a bare `rescue`, `StandardError` and its subclasses. `Interrupt`, `SystemExit` and `ScriptError`s such as `LoadError` propagate, so `require "x" rescue nil` still raises when the library is missing.
saying these in an interview costs you the question
- value = expr rescue fallback assigns the exception to value
- The rescue modifier can be limited to one class
- The rescue modifier catches every exception, including Interrupt
- a if b rescue c applies the rescue only to b
- The rescue modifier logs the exception it swallows