Ruby Exception Handling — begin rescue ensure raise and Custom Exceptions
In this tutorial, you will learn about Ruby Exception Handling. We cover key concepts, practical examples, and best practices to help you master this topic.
Ruby exception handling uses begin/rescue/ensure blocks for error recovery, raise for triggering errors, and custom exception classes for domain-specific error conditions.
What You'll Learn
- The begin/rescue/ensure/end structure
- Rescuing specific exception classes
- Raising custom exceptions
- The ensure block for cleanup
- Best practices for error handling
Why It Matters
Robust error handling separates production code from prototypes. Durga Antivirus Pro must handle corrupt files, permission errors, and network timeouts gracefully. DodaZIP catches compression errors and continues processing remaining files. Doda Browser handles network failures without crashing. Exception handling is how professional Ruby code stays reliable.
Real-World Use
Rails controllers rescue ActiveRecord::RecordNotFound to return 404s. Background Jobs rescue errors to retry later. API clients rescue network errors and provide meaningful messages. Every Ruby application needs strategic error handling.
flowchart LR
A["Exceptions"] --> B["begin/rescue"]
B --> C["Multiple Rescues"]
C --> D["Ensure"]
D --> E["Raise"]
E --> F["Custom Classes"]
A:::current --> B
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#dbeafe,stroke:#2563eb,color:#1e40af
style F fill:#f1f5f9,stroke:#94a3b8,color:#64748b
Basic begin/rescue
begin
result = 10 / 0
puts result
rescue ZeroDivisionError => e
puts "Cannot divide by zero: #{e.message}"
end
# Cannot divide by zero: divided by 0
Rescuing Specific Exceptions
begin
File.read("missing.txt")
rescue Errno::ENOENT => e
puts "File not found: #{e.message}"
rescue Errno::EACCES => e
puts "Permission denied: #{e.message}"
rescue => e # Catch-all
puts "Unknown error: #{e.class}: #{e.message}"
end
Rescue Without begin
You can rescue at the method level:
def divide(a, b)
a / b
rescue ZeroDivisionError
"Cannot divide by zero"
end
puts divide(10, 2) # 5
puts divide(10, 0) # Cannot divide by zero
Inline Rescue
# Risky — catches StandardError, too broad
result = risky_operation rescue "fallback"
# Better — more specific
result = begin
risky_operation
rescue SomeSpecificError
"fallback"
end
The Ensure Block
ensure runs whether or not an exception was raised:
file = nil
begin
file = File.open("data.txt", "r")
content = file.read
rescue Errno::ENOENT => e
puts "File not found"
ensure
file.close if file
puts "File closed"
end
Ensure with Return
def test_ensure
begin
return "from begin"
ensure
puts "ensure runs even after return"
end
end
puts test_ensure
# ensure runs even after return
# from begin
Raising Exceptions
# Simple raise
raise "Something went wrong"
# With exception class
raise ArgumentError, "Invalid argument"
# With existing exception
begin
raise "Original error"
rescue => e
raise RuntimeError, "Wrapped: #{e.message}"
end
raise vs fail
raise and fail are synonyms:
raise "error" # Most common
fail "error" # Same thing
Custom Exception Classes
class ApplicationError < StandardError; end
class ValidationError < ApplicationError; end
class AuthenticationError < ApplicationError; end
class NotFoundError < ApplicationError; end
def find_user(id)
raise NotFoundError, "User #{id} not found" unless id == 1
{ id: 1, name: "Alice" }
end
begin
user = find_user(99)
rescue NotFoundError => e
puts "Error: #{e.message}"
end
# Error: User 99 not found
Custom Exceptions with Additional Data
class ValidationError < StandardError
attr_reader :field, :value
def initialize(field, value, message = nil)
@field = field
@value = value
super(message || "Invalid value for #{field}: #{value.inspect}")
end
end
def validate_age(age)
raise ValidationError.new(:age, age, "Age must be positive") if age < 0
raise ValidationError.new(:age, age) if age > 150
true
end
begin
validate_age(-5)
rescue ValidationError => e
puts "#{e.field}: #{e.message}"
end
# age: Age must be positive
Exception Hierarchy
# Key exception classes
# BasicObject
# Exception
# NoMemoryError
# ScriptError
# LoadError
# NotImplementedError
# SyntaxError
# SignalException
# Interrupt
# StandardError <-- Most common rescue target
# ArgumentError
# IOError
# EOFError
# IndexError
# KeyError
# StopIteration
# NameError
# NoMethodError
# RangeError
# FloatDomainError
# RuntimeError
# TypeError
# ZeroDivisionError
# SystemExit
# SystemStackError
Retrying After Failure
attempts = 0
begin
attempts += 1
puts "Attempt #{attempts}"
raise "Failed" if attempts < 3
puts "Success"
rescue
retry if attempts < 3
end
# Attempt 1
# Attempt 2
# Attempt 3
# Success
The Else Block
else runs when no exception occurs:
begin
result = 10 / 2
rescue ZeroDivisionError => e
puts "Error: #{e.message}"
else
puts "Success: #{result}"
end
# Success: 5
Catch and Throw (Non-Local Exit)
For control flow (not error handling):
catch(:done) do
[1, 2, 3, 4, 5].each do |n|
throw(:done, "Found 3") if n == 3
end
end
# "Found 3"
Exception Information
begin
raise ArgumentError, "bad input"
rescue => e
puts e.message # "bad input"
puts e.class # ArgumentError
puts e.backtrace # Array of strings
puts e.full_message # Formatted message with backtrace
end
Common Mistakes
1. Rescuing Exception (Too Broad)
# Wrong — catches everything including SignalException, SystemExit
begin
something_risky
rescue Exception # Too broad!
puts "Error"
end
# Right — catch StandardError or specific classes
begin
something_risky
rescue StandardError => e
puts "Error: #{e.message}"
end
2. Swallowing Exceptions Silently
# Bad — you'll never know something went wrong
begin
something_risky
rescue
# Do nothing
end
# Better — at least log it
begin
something_risky
rescue => e
logger.error "Operation failed: #{e.message}"
end
3. Using Rescue as Control Flow
# Bad — exception handling is slow
begin
Integer(input)
rescue ArgumentError
nil
end
# Better — use check methods
Integer(input, exception: false) # Ruby 2.6+
4. Forgetting to Re-raise
def process_file(path)
begin
data = File.read(path)
transform(data)
rescue Errno::ENOENT => e
logger.error "File not found: #{path}"
raise # Re-raise the same exception
end
end
5. Ordering Rescue Blocks Wrong
begin
raise ArgumentError
rescue StandardError => e # Catches everything first!
puts "Standard error"
rescue ArgumentError => e # Never reached
puts "Argument error"
end
# Always put more specific exceptions first
Practice Questions
1. What's the difference between rescue and ensure?
rescue catches exceptions and handles recovery. ensure runs cleanup code regardless of whether an exception occurred. Use ensure for releasing resources (files, connections).
2. Why shouldn't you rescue Exception?
Exception is the root class. Rescuing it catches system-level exceptions like SignalException (Ctrl+C), NoMemoryError, and SystemExit — which should terminate the program.
3. What does retry do in a rescue block?
retry restarts the begin block from the beginning. Use it for transient failures with a retry limit. Without a limit, it loops forever.
4. How do you create a custom exception class?
Inherit from StandardError (or a more specific class): class MyError < StandardError; end. Add attributes if needed: constructors, reader methods, and custom messages.
Challenge: Create a robust file reader that retries up to 3 times with a 1-second delay between attempts, and returns nil if all attempts fail.
Solution
def robust_read(path, max_retries: 3)
retries = 0
begin
File.read(path)
rescue Errno::ENOENT => e
retries += 1
if retries <= max_retries
puts "Retry #{retries}/#{max_retries}: #{path}"
sleep 1
retry
else
puts "Failed after #{max_retries} retries"
nil
end
rescue Errno::EACCES => e
puts "Permission denied: #{path}"
nil
end
end
data = robust_read("/tmp/maybe_file.txt")
puts data.nil? ? "No data" : data.length
FAQ
{{< faq question="What's the difference between raise and fail?" >}}
Nothing. raise and fail are aliases for the same method. Most Ruby developers use raise. Some prefer fail when the exception is expected or part of normal control flow.
{{< /faq >}}
{{< faq question="Should I use exceptions for validation errors?" >}} Depends on context. In library code, raise exceptions for invalid input. In application code, use validation objects or return objects with error states. Rails uses exceptions for framework-level errors and validation objects for form errors. {{< /faq >}}
{{< faq question="Can I rescue an exception outside the begin block?" >}}
Yes. Method-level rescue blocks catch exceptions from any code in the method body without an explicit begin block. This is the most common pattern in Ruby.
{{< /faq >}}
{{< faq question="What happens if ensure has a return statement?" >}}
If ensure has a return, it overrides the exception and any previous return value. This is almost always a bug. Use ensure only for cleanup, not control flow.
{{< /faq >}}
{{< faq question="How do I add context to a re-raised exception?" >}}
Use a custom message and set the cause: raise MyError.new("context"), cause: original_exception. Or wrap: raise "Failed: #{e.message}" (the original exception's cause is preserved in Ruby 3.x).
{{< /faq >}}
Try It Yourself
# exceptions_demo.rb
class FileProcessor
class ProcessingError < StandardError; end
def self.process(path)
raise ProcessingError, "Path is nil" if path.nil?
raise ProcessingError, "File not found: #{path}" unless File.exist?(path)
content = File.read(path)
raise ProcessingError, "Empty file" if content.strip.empty?
content.upcase
rescue ProcessingError => e
puts "Processing error: #{e.message}"
nil
rescue => e
puts "Unexpected error: #{e.class}: #{e.message}"
nil
end
end
# Test
puts FileProcessor.process(nil)
puts FileProcessor.process("nonexistent.txt")
puts FileProcessor.process("/dev/null")
Expected output:
Processing error: Path is nil
Processing error: File not found: nonexistent.txt
Processing error: Empty file
What's Next
Now that you understand exception handling, learn about the Enumerable module in depth for powerful collection operations.
| Topic | Description | Link |
|---|---|---|
| Ruby Enumerable | each, map, select, reduce, group_by | {{< ref "21-enumerable" >}} |
| Ruby Date & Time | Time, Date, DateTime, strftime | {{< ref "22-date-time" >}} |
| Python Exceptions | Compare Python exception handling | Python |
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro