Ruby Exceptions
last modified October 18, 2026
Exceptions are Ruby's mechanism for signaling and handling errors. When something
goes wrong, Ruby raises an exception object, which unwinds the call stack until a
matching rescue clause handles it. This guide covers the exception
class hierarchy, the begin/rescue/else/ensure/retry
structure, raising and re-raising errors, defining custom exception classes, and
reading messages and backtraces.
Exception Hierarchy
All exceptions descend from the Exception class. Most application-level
errors inherit from StandardError, which is what a bare rescue
catches by default. Common subclasses include RuntimeError,
ArgumentError, ZeroDivisionError (a subclass of
StandardError), and NoMethodError (a subclass of
NameError). The example walks up the chain of superclasses from
ZeroDivisionError.
# Walk up the exception hierarchy klass = ZeroDivisionError while klass puts klass klass = klass.superclass end puts puts ZeroDivisionError.ancestors.include?(StandardError) puts RuntimeError.superclass puts ArgumentError.superclass puts NoMethodError.superclass puts StandardError.superclass
The superclass method returns the parent class, so repeating it climbs
the hierarchy. Rescuing a parent class also rescues all of its children, which is why
rescue => e (implicitly StandardError) is preferred over
rescuing Exception directly.
$ ./hierarchy.rb ZeroDivisionError StandardError Exception Object BasicObject true StandardError StandardError NameError Exception
begin, rescue, else, and ensure
A rescue clause can name one or several exception classes. The
else block runs only when no exception occurred, and ensure
runs in every case. Bare rescue without a class catches
StandardError and its subclasses.
def safe_divide(a, b)
a / b
rescue ZeroDivisionError => e
puts "Caught: #{e.class} - #{e.message}"
nil
else
puts "Division succeeded"
a / b
ensure
puts "Division attempted for #{a} / #{b}"
end
p safe_divide(10, 2)
p safe_divide(10, 0)
begin
Integer("abc")
rescue ArgumentError, TypeError => e
puts "Multiple classes: #{e.class}: #{e.message}"
end
In safe_divide, the division by zero is rescued and the method returns
nil, so the else block is skipped. The second call with
valid arguments runs else, which returns the result. The ensure
message appears in both calls, and it prints after the else output. Multiple classes are listed separated by commas, as
shown with ArgumentError, TypeError.
$ ./rescue_basics.rb Division succeeded Division attempted for 10 / 2 5 Caught: ZeroDivisionError - divided by 0 Division attempted for 10 / 0 nil Multiple classes: ArgumentError: invalid value for Integer(): "abc"
For the full syntax and rules of these clauses, see the rescue keyword and ensure keyword pages.
Raising and Re-raising Exceptions
The raise method accepts a message string, or a class with an optional
message, or an exception instance. Calling raise with no arguments inside
a rescue block re-raises the current exception, which lets you log an
error and still propagate it.
def check_age(age)
raise ArgumentError, "age must be positive" if age < 0
raise TypeError.new("age must be an Integer") unless age.is_a?(Integer)
age
end
[25, -1, 3.5].each do |value|
begin
puts "Valid: #{check_age(value)}"
rescue ArgumentError, TypeError => e
puts "#{e.class}: #{e.message}"
end
end
begin
begin
raise RuntimeError, "inner failure"
rescue RuntimeError => e
puts "Logging: #{e.message}"
raise
end
rescue => e
puts "Re-raised and caught outside: #{e.class} - #{e.message}"
end
The check_age method raises ArgumentError with a message, and
TypeError with an explicit instance. The nested block logs the error and
then re-raises it with bare raise, which the outer rescue
catches.
$ ./raise_reraise.rb Valid: 25 ArgumentError: age must be positive TypeError: age must be an Integer Logging: inner failure Re-raised and caught outside: RuntimeError - inner failure
See the raise keyword page for all of the
argument forms that raise accepts.
Custom Exception Classes
Applications usually define their own exception classes by inheriting from
StandardError. Extra attributes can carry context, such as the field
name and an error code, and super passes the message up to
Exception. A subclass with no body still works, and its default message
is the class name.
class ValidationError < StandardError
attr_reader :field, :code
def initialize(field, code)
@field, @code = field, code
super("invalid #{field}")
end
end
def register(username)
raise ValidationError.new(:username, 422) if username.length < 3
"Registered #{username}"
end
begin
register("ab")
rescue ValidationError => e
puts "Field: #{e.field}, code: #{e.code}"
puts "Message: #{e.message}"
end
class InsufficientFundsError < StandardError; end
puts InsufficientFundsError.new.message
ValidationError stores field and code as readers,
and its constructor builds the message and forwards it to super. The
rescued object exposes those attributes alongside message. The
InsufficientFundsError class is created without a message, so
message falls back to the class name.
$ ./custom_errors.rb Field: username, code: 422 Message: invalid username InsufficientFundsError
Retry
The retry keyword restarts the begin block from the top. It is
useful for transient failures, such as a temporary I/O problem, when a limited number
of attempts is acceptable. Always bound the retries with a counter to avoid an
infinite loop.
attempts = 0
begin
attempts += 1
puts "Attempt #{attempts}"
raise IOError, "temporary failure" if attempts < 3
puts "Succeeded after #{attempts} attempts"
rescue IOError => e
puts "Failed: #{e.message}"
retry if attempts < 3
puts "Giving up"
end
def fetch_data
yield
rescue RuntimeError => e
puts "fetch_data rescued: #{e.message}"
:fallback
end
p fetch_data { raise "network down" }
p fetch_data { :ok }
The first block retries twice before succeeding on the third attempt. The second
method uses yield inside a method body with rescue, which
returns :fallback when the block raises.
$ ./retry_example.rb Attempt 1 Failed: temporary failure Attempt 2 Failed: temporary failure Attempt 3 Succeeded after 3 attempts fetch_data rescued: network down :fallback :ok
Messages and Backtraces
Every exception responds to message for its text and class
for its type. The backtrace method returns an array of strings describing
the call stack at the point of the error. e.backtrace.first gives the most
recent frame, which is usually the most useful one for debugging. Avoid depending on
exact file paths or line numbers, since they vary between machines.
def risky
nil.upcase
end
begin
risky
rescue NoMethodError => e
puts "Class: #{e.class}"
puts "Message: #{e.message}"
puts "Backtrace is an Array: #{e.backtrace.is_a?(Array)}"
puts "First frame mentions risky: #{e.backtrace.first.include?('risky')}"
puts "Backtrace.first class: #{e.backtrace.first.class}"
end
begin
[1, 2].fetch(10)
rescue IndexError => e
puts "#{e.class}: #{e.message}"
end
The NoMethodError message describes the failing call on nil.
The first backtrace frame is a string that includes the name of the method where the
error occurred. The IndexError from fetch shows another
standard message format.
$ ./messages_backtrace.rb Class: NoMethodError Message: undefined method `upcase' for nil:NilClass Backtrace is an Array: true First frame mentions risky: true Backtrace.first class: String IndexError: index 10 outside of array bounds: -2...2
Source References
Learn more from these resources: Ruby Exception Documentation, Exceptions Syntax, and StandardError.
List all Ruby tutorials.
Author
My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.