Back to Scaling Ruby Applications guides

Exception Handling in Ruby

Stanley Ulili
Updated on October 20, 2025

Exception handling in Ruby helps you manage errors smoothly and keep your program stable when something unexpected happens. Instead of crashing, Ruby lets you control how errors move through your code and how your program reacts to them.

In Ruby, errors are treated as objects you can catch and handle. When a problem occurs, Ruby creates an exception object and looks for code that knows how to deal with it. It searches up the call stack until it finds a match or ends the program.

In this article, we'll look at Ruby's exception types, how the rescue and ensure blocks work, and how to create your own custom exceptions.

Let's dive in!

Prerequisites

You'll need Ruby 2.7 or later installed. Exception handling behavior has remained consistent across versions, though newer releases provide better error messages and debugging capabilities:

 
ruby --version
Output
ruby 3.4.5 (2025-07-16 revision 20cda200d3) +PRISM [arm64-darwin24]

Familiarity with Ruby methods and basic control flow helps you understand where exceptions fit in program execution, though we'll build concepts progressively:

 
ruby -e "puts 'Ready to handle exceptions like a pro!'"
Output
Ready to handle exceptions like a pro!

Setting up the environment

To demonstrate exception handling effectively, you'll build practical examples that reveal how Ruby manages errors in real scenarios. This exploration shows you exactly how exceptions propagate through your code and how different handling strategies affect program behavior and reliability.

What makes Ruby's exception system powerful is its balance between simplicity and control. Basic error handling requires minimal syntax, yet the system provides sophisticated tools for complex scenarios. By starting with common error situations and examining how Ruby responds, you'll develop intuition about when and how to handle exceptions.

Create your project directory:

 
mkdir ruby-exception-demo && cd ruby-exception-demo

Start with a foundation that demonstrates basic exception scenarios:

exception_basics.rb
def divide(a, b)
  a / b
end

puts divide(10, 2)
puts divide(10, 0)
puts "This line never runs"

This code demonstrates how exceptions interrupt normal execution. The first division succeeds, but dividing by zero creates a ZeroDivisionError that stops the program immediately.

When an exception occurs without a handler, Ruby prints the error details and terminates execution. This default behavior protects your application from continuing in an invalid state, but often you want more control over how errors are managed.

Run this to see unhandled exception behavior:

 
ruby exception_basics.rb
Output
5
exception_basics.rb:2:in 'Integer#/': divided by 0 (ZeroDivisionError)
        from exception_basics.rb:2:in 'Object#divide'
        from exception_basics.rb:6:in '<main>'

The error message shows the exception type, where it occurred, and the call stack. Ruby stopped executing when the exception occurred, never reaching the final line.

Understanding the exception hierarchy

Before handling exceptions effectively, you need to understand how Ruby organizes different error types into a class hierarchy. This structure determines which rescue clauses catch which exceptions and enables you to handle errors at the appropriate level of specificity. Every exception inherits from a base class, creating relationships that affect how your rescue logic behaves.

At the top of the hierarchy sits Exception, the parent class for all error conditions in Ruby. However, most application code should work with StandardError and its descendants rather than catching Exception directly. This distinction exists because some exceptions represent conditions where your program shouldn't continue, like interrupts or memory errors, while StandardError covers typical recoverable problems.

Common exception types you'll encounter include ArgumentError for invalid method arguments, TypeError for type mismatches, IOError for input/output problems, and RuntimeError for general runtime issues. Each serves a specific purpose in communicating what went wrong, helping you decide how to respond.

Create a new file to explore the exception hierarchy:

exception_hierarchy.rb
# ArgumentError - wrong number of arguments
begin
  [1, 2, 3].first(1, 2, 3)
rescue ArgumentError => e
  puts "ArgumentError: #{e.message}"
end

# TypeError - type mismatch
begin
  "hello" + 5
rescue TypeError => e
  puts "TypeError: #{e.message}"
end

# NoMethodError - calling undefined method
begin
  nil.upcase
rescue NoMethodError => e
  puts "NoMethodError: #{e.message}"
end

Each exception type communicates specific information about what went wrong. The hierarchy lets you catch related errors with a single rescue clause or handle each type differently based on your needs.

Run this to see different exception types in action:

 
ruby exception_hierarchy.rb
Output
ArgumentError: wrong number of arguments (given 3, expected 0..1)
TypeError: no implicit conversion of Integer into String
NoMethodError: undefined method 'upcase' for nil

Now extend the example to show how the hierarchy affects rescue behavior:

exception_hierarchy.rb
# Previous code...

# Catching at different hierarchy levels
def process_data(data)
data.upcase.reverse
rescue NoMethodError
"Can't process nil data"
rescue StandardError => e
"Unexpected error: #{e.class.name}"
end
puts process_data("hello")
puts process_data(nil)
puts process_data(123)

When you rescue StandardError, you catch most application errors but not system-level exceptions. More specific exception classes like TypeError take precedence when they appear first in your rescue clauses.

Run the complete example:

 
ruby exception_hierarchy.rb
Output
ArgumentError: wrong number of arguments (given 3, expected 0..1)
TypeError: no implicit conversion of Integer into String
NoMethodError: undefined method 'upcase' for nil
OLLEH
Can't process nil data
Can't process nil data

When you rescue StandardError, you catch most application errors. More specific exception classes like NoMethodError take precedence when they appear first in your rescue clauses.

Basic exception handling with rescue

The rescue clause gives you the primary mechanism for catching and responding to exceptions in Ruby. When you wrap potentially problematic code in a begin-rescue block, Ruby executes the code normally but jumps to your rescue handler if an exception occurs. This pattern lets you provide fallback behavior, log errors, or transform exceptions into more appropriate types for your application's context.

Ruby provides several syntaxes for rescue clauses depending on where they appear in your code. Method bodies support inline rescue without requiring explicit begin-end blocks, making error handling more concise. You can also chain multiple rescue clauses to handle different exception types with distinct logic.

The key to effective rescue usage is balancing specificity with maintainability. Catching exceptions too broadly makes it hard to understand what problems your code anticipates, while being overly specific creates verbose handlers. The goal is expressing clear intent about which errors you expect and how you'll respond to them.

Create a file demonstrating practical rescue patterns:

rescue_patterns.rb
# Basic rescue structure
def fetch_user(id)
  users = { 1 => "Alice", 2 => "Bob" }
  users.fetch(id)
rescue KeyError
  "Unknown user"
end

puts fetch_user(1)
puts fetch_user(99)

This example uses a rescue block to provide fallback behavior when a user doesn't exist, transforming the error into a meaningful response.

Run this to see basic rescue patterns:

 
ruby rescue_patterns.rb
Output
Alice
Unknown user

Now add examples showing rescue variations and access to exception objects:

rescue_patterns.rb
# Previous code...

# Multiple rescue clauses
def process_file(filename)
content = File.read(filename)
JSON.parse(content)
rescue Errno::ENOENT
puts "File not found"
{}
rescue JSON::ParserError
puts "Invalid JSON"
{}
end
require 'json'
puts process_file("data.json").inspect

Multiple rescue clauses let you handle different error types distinctly, checking each in order until one matches.

Run the complete example:

 
ruby rescue_patterns.rb
Output
Alice
Unknown user
File not found
{}

The file reading example demonstrates how rescue clauses form a chain that checks more specific exceptions before more general ones, ensuring each error type receives appropriate handling.

Ensuring cleanup with ensure and else

Beyond catching exceptions, you often need to guarantee that certain cleanup code runs regardless of whether errors occur. Ruby's ensure clause provides this guarantee, executing its code whether the begin block succeeds, raises an exception, or even encounters a return statement. This makes ensure perfect for releasing resources, closing files, or cleaning up state that must happen no matter what.

The else clause offers a complement to rescue by running only when no exceptions occur. This gives you a place to put success-specific logic that should execute after the protected code but only in the non-error case. The distinction between putting code at the end of a begin block versus in an else clause becomes important when ensure comes into play.

Together, rescue, else, and ensure create a complete flow control structure for exception handling. The begin block contains the code that might fail, rescue handles specific errors, else runs on success, and ensure handles cleanup. This structure makes the intent of your error handling explicit and ensures your code maintains consistency even when problems arise.

Create a comprehensive example showing these clauses working together:

ensure_patterns.rb
# Demonstrate ensure for resource cleanup
def write_log(message)
  file = File.open("app.log", "a")
  file.puts("#{Time.now}: #{message}")
  puts "Log written"
rescue IOError => e
  puts "Failed: #{e.message}"
ensure
  file&.close
  puts "File closed"
end

write_log("Application started")

The ensure block executes whether the file write succeeds or fails, guaranteeing proper resource cleanup.

Run this to see the complete flow:

 
ruby ensure_patterns.rb
Output
Log written
File closed

Now add an example showing ensure with early returns:

ensure_patterns.rb
# Previous code...

# Ensure runs even with early returns
def acquire_lock(resource)
puts "Lock acquired on #{resource}"
return "Successfully processed"
ensure
puts "Releasing lock"
end
result = acquire_lock("database")
puts "Result: #{result}"

The ensure clause executes even when return statements exit early, making it reliable for cleanup.

Run the complete example:

 
ruby ensure_patterns.rb
Output
Log written
File closed
Lock acquired on database
Releasing lock
Result: Successfully processed

The lock example demonstrates ensure running before the return, guaranteeing cleanup code executes regardless of how the method exits.

Raising and creating custom exceptions

While rescuing built-in exceptions handles many scenarios, sometimes you need to signal domain-specific errors that don't fit Ruby's standard exception types. Creating custom exceptions lets you communicate precise error conditions and handle them distinctly from generic errors. Raising exceptions explicitly gives you control over error propagation and enables you to enforce preconditions and invariants in your code.

Ruby's raise method (aliased as fail) creates and throws exception objects. You can raise exception classes directly, provide custom messages, or raise exception instances you've constructed with specific attributes. The flexibility in how you raise exceptions supports different coding styles while maintaining consistent exception handling semantics.

Custom exception classes inherit from StandardError or its descendants, allowing them to integrate smoothly with existing rescue clauses. By defining exception hierarchies specific to your domain, you create a vocabulary for different error conditions that makes your code more self-documenting and easier to handle appropriately.

Create a file demonstrating custom exceptions and raising strategies:

custom_exceptions.rb
# Define custom exception hierarchy
class ValidationError < StandardError; end
class EmailValidationError < ValidationError; end

def validate_user(email)
  raise EmailValidationError, "Invalid email" unless email.include?("@")
  "Valid user"
end

begin
  validate_user("alice@example.com")
  puts "Validation passed"
rescue ValidationError => e
  puts "Error: #{e.message}"
end

begin
  validate_user("invalid-email")
rescue EmailValidationError => e
  puts "Email error: #{e.message}"
end

Custom exceptions create meaningful error types specific to your application's domain. The hierarchy lets you catch all validation errors with one rescue or handle email and age errors separately.

Run this to see custom exceptions in action:

 
ruby custom_exceptions.rb
Output
Validation passed
Email error: Invalid email

Now add examples showing exception objects with state and re-raising:

custom_exceptions.rb
# Previous code...

# Custom exception with attributes
class InsufficientFundsError < StandardError
attr_reader :amount, :available
def initialize(amount, available)
@amount = amount
@available = available
super("Insufficient funds: $#{amount} requested, $#{available} available")
end
end
def withdraw(balance, amount)
raise InsufficientFundsError.new(amount, balance) if amount > balance
balance - amount
end
begin
withdraw(50, 100)
rescue InsufficientFundsError => e
puts e.message
puts "Shortfall: $#{e.amount - e.available}"
end

Exception objects can carry state beyond their message, providing context about what went wrong.

Run the complete example:

 
ruby custom_exceptions.rb
Output
Validation passed
Email error: Invalid email
Insufficient funds: $100 requested, $50 available
Shortfall: $50

The insufficient funds example shows how custom exception attributes provide programmatic access to error details beyond the message string.

Final thoughts

Ruby’s exception system helps you build reliable programs that handle errors smoothly while keeping your code clean and easy to read. The main idea is to know when to rescue an exception, when to let it continue, and how to use ensure blocks to run cleanup code every time. Good error handling focuses on expected problems and lets unexpected ones show clear error messages.

Exception handling works best when your API design treats exceptions as rare events, not part of normal logic. This keeps your main code simple and your rescue blocks focused on real issues. Check out the Ruby Exception documentation and try creating your own exception classes to learn how to organize error handling in your projects.

Got an article suggestion? Let us know
Next article
Top 10 Ruby on Rails Alternatives for Web Development
Discover the top 10 Ruby on Rails alternatives. Compare Django, Laravel, Express.js, Spring Boot & more with detailed pros, cons & features.
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.