ZetCode

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.

hierarchy.rb
# 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.

rescue_basics.rb
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.

raise_reraise.rb
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.

custom_errors.rb
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.

retry_example.rb
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.

messages_backtrace.rb
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.