A bare rescue catches StandardError, not everything¶
Level: 201 · anyone who has written rescue => e and wondered what it misses
One line: rescue with no class list means rescue StandardError, and SystemExit, Interrupt, NoMemoryError, SystemStackError and every ScriptError — LoadError, SyntaxError and, surprisingly, NotImplementedError — sit outside that branch, which is why exit still exits through a bare rescue and why rescue Exception is the anti-pattern.
Every Ruby exception descends from Exception, but the tree has two halves. StandardError heads the half a program is expected to handle: RuntimeError, ArgumentError, TypeError, NameError, KeyError, IOError, the Errno family and their subclasses. The other half is for things the interpreter is telling you about — the process is exiting (SystemExit), a signal arrived (Interrupt), memory or stack ran out (NoMemoryError, SystemStackError), or the code itself could not be loaded or is unfinished (ScriptError, with LoadError, SyntaxError and NotImplementedError under it). A rescue clause with no class list catches StandardError and nothing above it, so the second half passes straight through — which is what you want: exit inside a rescued block still exits, Ctrl-C still stops the program, and a method left as raise NotImplementedError is not quietly turned into a return value. rescue Exception catches both halves, and the program shows what that does to exit.
Verified output of standard_error_is_the_default_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
1. the hierarchy, classes only, up to Exception
StandardError StandardError < Exception
RuntimeError RuntimeError < StandardError < Exception
ArgumentError ArgumentError < StandardError < Exception
TypeError TypeError < StandardError < Exception
NameError NameError < StandardError < Exception
NoMethodError NoMethodError < NameError < StandardError < Exception
ZeroDivisionError ZeroDivisionError < StandardError < Exception
KeyError KeyError < IndexError < StandardError < Exception
StopIteration StopIteration < IndexError < StandardError < Exception
FrozenError FrozenError < RuntimeError < StandardError < Exception
IOError IOError < StandardError < Exception
Errno::ENOENT Errno::ENOENT < SystemCallError < StandardError < Exception
SystemStackError SystemStackError < Exception
NotImplementedError NotImplementedError < ScriptError < Exception
LoadError LoadError < ScriptError < Exception
SyntaxError SyntaxError < ScriptError < Exception
SystemExit SystemExit < Exception
Interrupt Interrupt < SignalException < Exception
NoMemoryError NoMemoryError < Exception
2. what a bare rescue catches (a bare rescue inside, rescue Exception outside)
RuntimeError caught by the bare rescue
ArgumentError caught by the bare rescue
StopIteration caught by the bare rescue
Errno::ENOENT caught by the bare rescue
SystemExit SLIPPED PAST it; rescue Exception caught SystemExit
Interrupt SLIPPED PAST it; rescue Exception caught Interrupt
NoMemoryError SLIPPED PAST it; rescue Exception caught NoMemoryError
NotImplementedError SLIPPED PAST it; rescue Exception caught NotImplementedError
LoadError SLIPPED PAST it; rescue Exception caught LoadError
SystemStackError SLIPPED PAST it; rescue Exception caught SystemStackError
3. a bare rescue is rescue StandardError
StandardError === e true
Exception.new.is_a?(StandardError) false
StandardError.superclass Exception
4. rescue Exception swallows exit (the anti-pattern)
swallowed SystemExit (status 1); the program goes on
5. a real stack overflow is not a StandardError
slipped past the bare rescue: SystemStackError: stack level too deep
6. a method left as raise NotImplementedError is not caught either
slipped past the bare rescue: NotImplementedError: Shape must define area
Reading the output¶
Row 1 prints each class's ancestors, classes only (the mix-ins DidYouMean::Correctable and ErrorHighlight::CoreExt are filtered out), stopping before Object. Read the right-hand end of each line: the first twelve chains pass through StandardError, the other seven do not. A few placements are worth remembering — KeyError and StopIteration are IndexErrors, FrozenError is a RuntimeError, Errno::ENOENT is a SystemCallError — and two are traps: SystemStackError hangs directly off Exception, and NotImplementedError is a ScriptError, beside LoadError and SyntaxError, not a StandardError.
Row 2 raises ten of those classes inside a bare rescue that sits inside a rescue Exception. The first four are caught where you would expect; the other six say SLIPPED PAST it — the bare rescue never ran, and only the outer rescue Exception stopped them. Row 3 states the rule the row demonstrates: a bare rescue is rescue StandardError, Exception itself is not a StandardError, and StandardError's superclass is Exception.
Row 4 is the anti-pattern in one line: exit 1 raises SystemExit, rescue Exception catches it, and the program carries on with the message printed — the process that was told to stop did not. The same clause would swallow a Ctrl-C (Interrupt) and a stack overflow. Rows 5 and 6 are the two cases that bite in real code: a method that calls itself without end raises SystemStackError ("stack level too deep"), and an abstract method left as raise NotImplementedError raises a ScriptError — both pass through rescue => e untouched, and the output shows only the outer, specific rescue seeing them. If you want to catch a NotImplementedError, name it.
Compared with Python¶
Python's tree has the same two halves but draws the line elsewhere and — the important part — its bare form catches the wrong half. BaseException is the root; Exception is the branch a program handles; only SystemExit, KeyboardInterrupt and GeneratorExit (plus BaseExceptionGroup) sit outside it. Row 1 shows the placements: KeyError and IndexError are LookupErrors, ZeroDivisionError is an ArithmeticError, FileNotFoundError is an OSError, and RecursionError, NotImplementedError and MemoryError are all ordinary Exceptions — the three Ruby keeps outside StandardError. Row 2 confirms it: except Exception catches MemoryError, NotImplementedError and RecursionError, and only SystemExit and KeyboardInterrupt slip past. Row 3 is the reversal: a bare except: is except BaseException:, so it does catch SystemExit — Ruby's bare rescue is the safe default, Python's bare except: is the anti-pattern, and except Exception: is the Python spelling of Ruby's rescue => e. Row 4 shows the consequence — sys.exit(1) swallowed by a bare except:. Rows 5 and 6 print the opposite of the Ruby rows: a real stack overflow (RecursionError, a RuntimeError) and a NotImplementedError (also a RuntimeError) are both caught by except Exception.
Verified output of standard_error_is_the_default_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. the hierarchy, up to BaseException
Exception Exception < BaseException
RuntimeError RuntimeError < Exception < BaseException
ValueError ValueError < Exception < BaseException
TypeError TypeError < Exception < BaseException
NameError NameError < Exception < BaseException
AttributeError AttributeError < Exception < BaseException
ZeroDivisionError ZeroDivisionError < ArithmeticError < Exception < BaseException
KeyError KeyError < LookupError < Exception < BaseException
StopIteration StopIteration < Exception < BaseException
IndexError IndexError < LookupError < Exception < BaseException
OSError OSError < Exception < BaseException
FileNotFoundError FileNotFoundError < OSError < Exception < BaseException
RecursionError RecursionError < RuntimeError < Exception < BaseException
NotImplementedError NotImplementedError < RuntimeError < Exception < BaseException
ModuleNotFoundError ModuleNotFoundError < ImportError < Exception < BaseException
SyntaxError SyntaxError < Exception < BaseException
SystemExit SystemExit < BaseException
KeyboardInterrupt KeyboardInterrupt < BaseException
MemoryError MemoryError < Exception < BaseException
2. what except Exception catches (except Exception inside, except BaseException outside)
RuntimeError caught by except Exception
ValueError caught by except Exception
StopIteration caught by except Exception
FileNotFoundError caught by except Exception
SystemExit SLIPPED PAST it; except BaseException caught SystemExit
KeyboardInterrupt SLIPPED PAST it; except BaseException caught KeyboardInterrupt
MemoryError caught by except Exception
NotImplementedError caught by except Exception
ModuleNotFoundError caught by except Exception
RecursionError caught by except Exception
3. a bare except is except BaseException -- the opposite default
bare except caught SystemExit; isinstance(e, Exception): False
isinstance(Exception(), BaseException) True
Exception.__mro__[1].__name__ BaseException
4. a bare except swallows sys.exit (the anti-pattern)
swallowed SystemExit (code 1); the program goes on
5. a real stack overflow IS an Exception here
except Exception caught RecursionError
6. NotImplementedError is a RuntimeError, so except Exception catches it
except Exception caught NotImplementedError
KeyboardInterrupt is what Python's default SIGINT handler raises, and the Python library's Ctrl-C is a signal ↗ shows how signal.signal() replaces that handler; Ruby's Interrupt is the same idea, and the exit lesson here measures it in a child process.
Try it¶
- Write
safe_call, a method that yields to its block and returns[:ok, value], or[:error, e.class]when the block raises aStandardError. Show it on a good block, onInteger("abc")and on{}.fetch(:missing); then show thatsafe_call { exit 2 }lets theSystemExitthrough — catch it at the top level withrescue SystemExit => eand printe.status. - Write
retry_on(*classes, times:)that yields and retries only when the block raises one of the listed classes, re-raising anything else at once. Test it with a block that raisesArgumentErrortwice and then succeeds, and with a block that raisesNotImplementedError.
Solution to 1
standard_error_is_the_default_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.
# Exercise 1: safe_call runs a block and turns a StandardError into a value, while
# letting exit (SystemExit) pass straight through to the top level.
def safe_call
[:ok, yield]
rescue StandardError => e
[:error, e.class]
end
puts "1. a value comes back tagged :ok"
p safe_call { 21 * 2 }
puts "2. a StandardError comes back tagged :error"
p safe_call { Integer("abc") }
p safe_call { {}.fetch(:missing) }
puts "3. exit is not a StandardError, so it passes through safe_call"
begin
safe_call { exit 2 }
puts " never printed"
rescue SystemExit => e
puts " SystemExit reached the top level with status #{e.status}"
end
Verified output of standard_error_is_the_default_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
See also¶
rescue,elseandensurerun in a fixed order — the clause whose default class this page names- Custom exceptions subclass
StandardError— where your own classes go in the tree, and why not underException exitraisesSystemExit— the exception a barerescuelets through, measured in child processes- Common error messages — every class in row 1 with the code that raises it and the message it prints
- Ctrl-C is a signal ↗ — the Python library on
KeyboardInterrupt, Python'sInterrupt - Exception ↗ — the Ruby reference, with the full built-in hierarchy
raisehas four forms —raise "msg"is aRuntimeError,raise Klassan instance whose message is the class name, `raise…rescueworks as a modifier, forStandardErroronly —expr rescue fallbackevaluatesexprand, if it raises aStandardError, yieldsfallbackinstead; it…throwandcatchare not exceptions —catch(:tag) { … throw :tag, value … }is a labelled non-local jump:throwunwinds to the matching…- Nothing matched:
case/inraises,case/whenreturnsnil— Acase/inwith no matching branch and noelse, and a rightwardvalue => pattern, raise… Timeout.timeoutinterrupts a thread from outside —Timeout.timeout(0.2) { sleep 5 }raisesTimeout::Errorafter a fifth of a second because a watcher…