rescue works as a modifier, for StandardError only¶
Level: 201 · anyone tempted to write value = risky rescue default
One line: expr rescue fallback evaluates expr and, if it raises a StandardError, yields fallback instead; it binds looser than assignment, cannot stand bare in an argument list, and hides every StandardError alike — a typo's NoMethodError as readily as the ArgumentError you meant.
rescue has a one-line form, in the family of if and unless modifiers: expr rescue fallback. It catches StandardError — the same default as a bare rescue clause — and nothing else, so a NotImplementedError or a SystemExit passes through it. It binds after assignment: x = expr rescue 0 is x = (expr rescue 0), which is the reading you want, and in an argument list it needs its own parentheses because a bare p(expr rescue 0) does not parse. Two related rules concern blocks: since Ruby 2.5 a do … end block may carry rescue, else and ensure clauses like a method body, but a { } block may not — the modifier is the only rescue a brace block can hold. The form is popular for parsing — Integer(text) rescue 0 — and the same program shows the alternative that does not swallow unrelated errors: Integer(text, exception: false) returns nil for bad input and still raises for everything else.
Verified output of rescue_as_a_modifier_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
1. expr rescue fallback
Integer("abc") rescue 0 -> 0
Integer("42") rescue 0 -> 42
2. it catches StandardError only
(raise NotImplementedError) rescue 0 -> propagated NotImplementedError
3. how it binds
value = raise("boom") rescue "fallback" -> value is "fallback"
as a bare argument, p(expr rescue 0): SyntaxError
with its own parentheses, p((expr rescue 0)): compiles
4. a rescue clause in a def needs no begin
parse("q") -> "method-level rescue"
5. a rescue clause inside do...end works; inside { } it is a syntax error
do |x| ... rescue => e ... end compiles
{ |x| ... rescue => e ... } SyntaxError
rescued inside do...end: "inside a do block (1)"
6. the modifier form is fine inside { }
modifier inside braces (1)
7. Integer() raises where to_i does not
"abc".to_i -> 0
"12abc".to_i -> 12
Integer("abc") -> ArgumentError: invalid value for Integer(): "abc"
Integer("abc", exception: false) -> nil
8. the trap: the modifier hides every StandardError, including a typo
nil.upcse rescue "default" -> "default" (the NoMethodError from the typo is gone)
Reading the output¶
Row 1 is the form in use: Integer("abc") raises ArgumentError, so the expression yields 0; Integer("42") does not raise, so the fallback is never evaluated and x is 42. Row 2 raises a NotImplementedError under the modifier and it comes straight through to the outer rescue NotImplementedError — the modifier is rescue StandardError, nothing wider. Row 3 is precedence: value = raise("boom") rescue "fallback" leaves value holding "fallback", because the modifier binds before the assignment does; and the compiles? helper (which asks RubyVM::InstructionSequence.compile and reports the class of the failure) shows that p(expr rescue 0) is a SyntaxError while p((expr rescue 0)) is fine.
Row 4 is the method-level clause from the first lesson, for contrast with the one-liner. Row 5 compiles two blocks that differ only in their delimiters: the do … end block with a rescue => e clause compiles and, when run, rescues; the { } block with the same clause is a SyntaxError. Row 6 shows the way round it — inside braces, the modifier form is legal. Row 7 is why the modifier is so often wrapped around Integer(): to_i never raises (it reads "12abc" as 12 and "abc" as 0), Integer() refuses anything that is not a whole number literal, and Integer(text, exception: false) gives nil without a rescue at all.
Row 8 is the argument against the modifier. nil.upcse is a typo for upcase; it raises NoMethodError, a StandardError, and the modifier turns it into "default". The bug is now invisible. The rule of thumb: use the modifier only when the expression can raise exactly one kind of error you have already thought about, and prefer exception: false, fetch with a default, or dig where they exist.
Compared with Python¶
Python has no expression form of try at all — row 1 compiles x = int('abc') except ValueError: 0 and gets a SyntaxError; that syntax was proposed in PEP 463 and rejected. (Not machine-checked here.) The idioms in its place are a small helper (or_default(f, default), which takes a lambda), and contextlib.suppress, which is a with block rather than an expression: row 2 shows it catching only the class it is given, and row 3 the consequence of it being a statement — when the suppressed line raises, the rest of the block is skipped, assignment included, so the default has to be set before the block. Row 4 matches Ruby's row 4 only in spirit: there is no function-level except, so the try is written out. Row 5 is Python's version of the brace-block rule: try cannot appear inside a lambda or any other expression, with or without parentheses. Row 6 lists what Python reaches for instead of a catch-all: dict.get, getattr with a default, next(it, default). Row 7 is the parsing difference: int() has no to_i beside it — "abc" and "12abc" both raise ValueError, and only surrounding whitespace is forgiven. Row 8 is the same trap in the same shape: a catch-all helper turns the typo's AttributeError into 'default'.
Verified output of rescue_as_a_modifier_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. no expression form: try is a statement
x = int('abc') except ValueError: 0 -> SyntaxError
or_default(lambda: int('abc'), 0) -> 0
or_default(lambda: int('42'), 0) -> 42
2. contextlib.suppress catches only what it is given
suppress(ValueError) around raise NotImplementedError -> propagated NotImplementedError
3. how it binds: suppress skips the rest of its block, assignment included
x after a suppressed int('abc') -> 'unchanged' (set the default before the block)
4. no def-level except: the try is written out
parse('q') -> 'function-level except'
5. try cannot appear in a lambda or any expression
f = lambda: try: 1 except: 2 SyntaxError
f = lambda: (try: 1 except: 2) SyntaxError
6. the built-in defaults that replace most rescue modifiers
d.get('b', 0) -> 0
getattr(None, 'upper', 'dflt') -> 'dflt'
next(iter([]), 'empty') -> 'empty'
7. int() raises where Ruby's to_i does not
int('abc' ) -> ValueError
int('12abc' ) -> ValueError
int(' 42 ' ) -> 42
8. the same trap: a catch-all hides a typo
or_default(lambda: None.upcse(), 'default') -> 'default' (the AttributeError is gone)
The Ruby text library's to_i never fails; Integer() does ↗ covers row 7 in depth — what each parser accepts, including 0x and 0b prefixes and non-ASCII digits. contextlib.suppress ↗ and PEP 463 ↗ are the Python references.
Try it¶
- Write
to_number(text)that returnsInteger(text), orFloat(text)when that fails, ornilwhen both fail, using the modifier form for each step. Run it on"42","4.2","abc","0x1f","1e3"and"". - Rewrite
to_numberwithexception: falseinstead of the modifier, then introduce a typo (Integr(text)) into both versions and show which one reports theNameErrorand which one silently returnsnil.
Solution to 1
rescue_as_a_modifier_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.
# Exercise 1: to_number tries Integer(), then Float(), then gives up with nil, using
# the modifier form for each step.
def to_number(text)
Integer(text) rescue (Float(text) rescue nil)
end
["42", "4.2", "abc", "0x1f", "1e3", ""].each do |text|
puts " #{text.inspect.ljust(7)} -> #{to_number(text).inspect}"
end
Verified output of rescue_as_a_modifier_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
See also¶
rescue,elseandensurerun in a fixed order — the full form the modifier abbreviates- A bare
rescuecatchesStandardError, not everything — the class the modifier catches, and the ones it lets through - Common error messages — the
NoMethodErrorandArgumentErrorthat rows 7 and 8 raise - The command-line flags —
ruby -c, the syntax check thecompiles?helper does in-process to_inever fails;Integer()does ↗ — the Ruby text library on row 7unlessnegatesif; modifiers hang off the end of a line — Ruby spellsif notasunlessandwhile notasuntil, lets any statement carryif,unless,…