Introduction
Ruby organizes all errors into a class hierarchy rooted at Exception. Understanding this tree is the first step to writing robust error handling, because rescuing the wrong class can make your program impossible to interrupt or debug.
Key Concepts
- Exception: The root class of all Ruby errors. You should almost never rescue this directly.
- StandardError: The default class rescued by bare
rescue. Your custom errors should inherit from this. - RuntimeError: The default error raised by
raisewith just a string message.
Real World Context
Imagine you deploy a long-running background worker. If you rescue Exception instead of StandardError, pressing Ctrl+C or sending a SIGTERM to gracefully shut down the process will be swallowed by your rescue block. Your process becomes unkillable without kill -9, and deployments grind to a halt.
Deep Dive
Ruby's exception hierarchy is a class tree. Every rescuable error descends from Exception, but the ones you typically care about live under StandardError.
Here is the top-level structure of the hierarchy:
Exception
├── NoMemoryError
├── ScriptError
├── SignalException
├── SystemExit
└── StandardError <-- rescue this!
├── ArgumentError
├── IOError
├── NameError
├── RuntimeError <-- default raise type
├── TypeError
└── (your custom errors)
The key insight is that StandardError and its children represent errors your application can reasonably recover from, while everything else represents system-level signals that should propagate.
Never Rescue Exception
Rescuing Exception traps system signals like SystemExit and Interrupt. The following example shows the dangerous pattern:
ruby# BAD - catches Ctrl+C, kill signals, etc. begin do_something rescue Exception => e # This catches EVERYTHING end
This is dangerous because your code will silently swallow signals the operating system uses to manage your process. Instead, use a bare rescue which defaults to StandardError:
ruby# GOOD - only catches StandardError and subclasses begin do_something rescue => e # Defaults to StandardError puts e.message end
Notice that the bare rescue => e is equivalent to rescue StandardError => e. This is idiomatic Ruby.
Specific Rescues
You can rescue multiple exception types in order from most specific to least specific:
rubybegin risky_operation rescue ArgumentError => e puts "Bad argument: \#{e.message}" rescue IOError => e puts "I/O problem: \#{e.message}" rescue => e puts "Other error: \#{e.message}" end
Ruby checks rescue clauses top-to-bottom and runs the first one that matches. Always put more specific exceptions first, otherwise they will be shadowed by a broader rescue above them.
Common Pitfalls
- Rescuing Exception instead of StandardError — This catches
Interrupt,SystemExit, andNoMemoryError, making your process nearly impossible to stop gracefully. Always use barerescueor explicitly rescueStandardError. - Ordering rescue clauses from broad to narrow — If you put
rescue StandardErrorbeforerescue ArgumentError, the specific clause will never execute becauseArgumentErroris a subclass ofStandardError.
Best Practices
- Default to bare rescue — A bare
rescue => ecatchesStandardErrorand its subclasses, which is almost always what you want. - Rescue the narrowest class possible — Catching only the errors you expect makes debugging easier and prevents masking unexpected bugs.
Summary
- Ruby's exception hierarchy has
Exceptionat the root, but you should rescueStandardError(or its subclasses) in application code. - Never rescue
Exceptiondirectly — it traps system signals and makes your process hard to manage. - Order rescue clauses from most specific to least specific so that each handler gets a chance to match.
Code Examples
# Demonstrating the exception hierarchy
puts ArgumentError.ancestors
# => [ArgumentError, StandardError, Exception, Object, Kernel, BasicObject]
puts SignalException.ancestors
# => [SignalException, Exception, Object, Kernel, BasicObject]
# Safe rescue pattern
begin
Integer("not_a_number")
rescue ArgumentError => e
puts "Caught: #{e.message}"
# => Caught: invalid value for Integer(): "not_a_number"
end