Exception Handling
Reviewed & published by Brayan K
Ruby is a dynamic, beginner-friendly programming language, and exception handling lets your program respond to errors gracefully — catching problems with begin/rescue instead of crashing.
Part of the free Ruby course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
By the end of this lesson you'll rescue specific errors, guarantee cleanup with ensure, raise and define your own exceptions, and retry failed operations.
What You'll Learn in This Lesson
- Wrapping risky code in begin/rescue
- Rescuing specific exception classes
- Reading the error object and its message
- Guaranteed cleanup with ensure
- Raising errors and defining custom ones
- Retrying with retry safely
1️⃣ begin, rescue, and the Error Object
Put risky code in a begin block; if it raises, the matching rescue runs instead of crashing. Capture the error with => e to read its message. Always rescue the most specific class you can.
# Wrap risky code in begin/rescue so a crash becomes a handled case.
begin
result = 10 / 0 # raises ZeroDivisionError
puts result
rescue ZeroDivisionError => e
puts "Caught: #{e.message}" # -> Caught: divided by 0
end
puts "Program keeps running" # -> Program keeps running (no crash!)
# Rescue specific error types, most specific first:
def parse_age(text)
begin
Integer(text) # raises ArgumentError on bad input
rescue ArgumentError
puts "'#{text}' is not a number"
nil
end
end
puts parse_age("42").inspect # -> 42
puts parse_age("oops").inspect # -> 'oops' is not a number then nil2️⃣ ensure, raise, Custom Errors, and retry
ensure always runs — ideal for cleanup. raise throws an error, and you can define your own error classes by subclassing StandardError for clarity. retry re-runs the block — guard it with a counter so it can't loop forever.
# ensure ALWAYS runs — success or failure. Perfect for cleanup.
def read_config
puts "opening resource"
raise "config missing" # raise creates and throws an error
rescue => e # bare rescue catches StandardError
puts "handled: #{e.message}"
ensure
puts "closing resource" # runs no matter what
end
read_config
# -> opening resource / handled: config missing / closing resource
# Define and raise your OWN exception classes for clear error types:
class WithdrawalError < StandardError; end
def withdraw(balance, amount)
raise WithdrawalError, "Insufficient funds" if amount > balance
balance - amount
end
begin
withdraw(50, 100)
rescue WithdrawalError => e
puts "Denied: #{e.message}" # -> Denied: Insufficient funds
end
# retry re-runs the begin block (use a counter to avoid infinite loops):
attempts = 0
begin
attempts += 1
raise "flaky" if attempts < 3
puts "Succeeded on attempt #{attempts}" # -> Succeeded on attempt 3
rescue
retry if attempts < 3
endYour turn. Make a safe_divide method that never crashes and always logs an attempt. Complete the TODO rescue, then run it.
# 🎯 YOUR TURN — replace each TODO, then run with: ruby safe_divide.rb
def safe_divide(a, b)
begin
result = a / b
"#{a} / #{b} = #{result}"
rescue ZeroDivisionError # 👉 TODO: catch the divide-by-zero error
"Cannot divide by zero"
ensure
puts "(division attempted)" # 👉 runs every time
end
end
puts safe_divide(10, 2)
puts safe_divide(10, 0)
# ✅ Expected output:
# (division attempted)
# 10 / 2 = 5
# (division attempted)
# Cannot divide by zeroMini-Challenge: A Validated Age Reader
Parse and validate input, raising and rescuing an ArgumentError for bad values. Run with ruby age.rb.
# 🎯 MINI-CHALLENGE: a validated age reader.
# Save as age.rb and run with: ruby age.rb
#
# 1. Write a method get_age(text) that uses Integer(text) to parse.
# 2. If the value is negative, raise an ArgumentError with a message.
# 3. rescue ArgumentError and return the string "invalid".
# 4. Test it with "30", "-5", and "abc".
#
# ✅ Example output:
# 30
# invalid
# invalid
# your code hereCommon Errors (and the fix)
- rescue Exception — Too broad; it swallows signals and system errors. Rescue StandardError or a specific class.
- Swallowing errors silently — An empty rescue hides bugs. At least log e.message.
- retry without a guard — Re-running on a persistent error loops forever. Cap attempts with a counter.
- Wrong rescue order — A broad rescue before a specific one shadows it. List specific classes first.
- Using exceptions for normal flow — Don't raise for ordinary conditions; reserve exceptions for genuine errors.
Pro Tips
- 💡 Method-level rescue: you can put rescue directly in a def without a begin.
- 💡 Custom error hierarchies: a base AppError lets callers rescue all your errors at once.
- 💡 Inspect the backtrace: e.backtrace shows where the error originated.
📋 Quick Reference — Exceptions
| Concept | Ruby Syntax |
|---|---|
| Handle | begin ... rescue ... end |
| Capture error | rescue => e |
| Always run | ensure ... end |
| Throw error | raise "msg" |
| Custom error | class E < StandardError |
| Retry block | retry |
🎉 Lesson Complete!
- ✅ begin/rescue turns crashes into handled cases
- ✅ Rescue the most specific exception class you can
- ✅ Capture the error with => e and read e.message
- ✅ ensure always runs — use it for cleanup
- ✅ raise throws; subclass StandardError for custom errors
Practice quiz
Which error does `10 / 0` raise in Ruby?
- ArgumentError
- TypeError
- ZeroDivisionError
- RuntimeError
Answer: ZeroDivisionError. Dividing an integer by zero raises ZeroDivisionError with the message 'divided by 0'.
What block catches a raised error so the program keeps running?
- begin/rescue
- begin/ensure
- try/catch
- if/else
Answer: begin/rescue. begin/rescue wraps risky code; the rescue runs instead of crashing.
How do you capture the error object to read its message?
- rescue e:
- catch e
- rescue.message
- rescue => e, then e.message
Answer: rescue => e, then e.message. Use `rescue SomeError => e`, then read e.message.
When does the `ensure` clause run?
- Only on success
- Whether or not an error happened
- Only on failure
- Only after retry
Answer: Whether or not an error happened. ensure always runs — success, rescued error, or unrescued error — so it's ideal for cleanup.
Why avoid `rescue Exception`?
- It is too broad and swallows signals and system errors
- It is a syntax error
- It can't read messages
- It is slower
Answer: It is too broad and swallows signals and system errors. rescue Exception catches everything including Ctrl-C and out-of-memory, which you should not swallow. Rescue StandardError or a specific class.
How do you define your own exception type?
- def WithdrawalError
- error WithdrawalError
- class WithdrawalError < StandardError; end
- raise class WithdrawalError
Answer: class WithdrawalError < StandardError; end. Subclass StandardError: `class WithdrawalError < StandardError; end`.
What does `retry` do, and how should you guard it?
- Re-raises; guard with ensure
- Re-runs the begin block; cap attempts with a counter
- Exits; no guard needed
- Skips the rescue; use a flag
Answer: Re-runs the begin block; cap attempts with a counter. retry re-runs the begin block; without an attempt counter it can loop forever.
What does `Integer("oops")` raise?
- ZeroDivisionError
- It returns nil
- TypeError
- ArgumentError
Answer: ArgumentError. Integer() raises ArgumentError when the string isn't a valid number.
Where can a `rescue` clause live without an explicit `begin`?
- Nowhere
- Directly inside a method (def) body
- Only at the top level
- Inside a class but not a method
Answer: Directly inside a method (def) body. A method body acts as the begin block, so you can write rescue directly inside a def.
What does an empty `rescue` with no logging risk?
- A crash
- Running ensure twice
- Silently hiding bugs
- An infinite loop
Answer: Silently hiding bugs. Swallowing errors silently hides bugs; at least log e.message.
Continue this course
- Previous: Modules & Mixins
- Next: Regular Expressions — Match, extract, and replace text patterns with regex
- Quick reference: Ruby cheat sheet
Frequently asked questions
What's the difference between begin/rescue and just letting code crash?
If an exception is raised and nothing rescues it, your program prints a backtrace and stops. Wrapping risky code in begin/rescue lets you catch the error, respond gracefully — log it, return a default, ask the user to retry — and keep running. The goal isn't to hide errors but to handle the ones you can recover from. In Ruby a method body can also act as the begin block, so you can write rescue directly inside a def without an explicit begin.
What does the ensure clause do?
ensure runs no matter how the block exits — whether it succeeded, raised an error that was rescued, or raised one that wasn't. It's the place for cleanup that must always happen: closing a file, releasing a lock, disconnecting from a database. Because it always runs, you avoid leaking resources even when something goes wrong. Many built-in methods like File.open with a block handle this for you, but ensure is the tool when you manage a resource yourself.
Should I rescue a specific error or a generic one?
Rescue the most specific exception you can, like ZeroDivisionError or ArgumentError, so you only catch problems you actually understand and intend to handle. A bare rescue (with no class) catches StandardError and its subclasses, which is fine for a broad safety net but risks hiding bugs you'd rather see. Avoid rescue Exception — that catches everything, including signals and out-of-memory errors you should not swallow. List specific rescues first, then a broader one as a fallback if needed.
How do I make my own exception types?
Define a class that inherits from StandardError: class WithdrawalError < StandardError; end. Then raise it with a message: raise WithdrawalError, "Insufficient funds". Custom exception classes make your error handling expressive — callers can rescue WithdrawalError specifically rather than guessing from a generic message. It's a common pattern in libraries: a small hierarchy of named errors that describe exactly what went wrong, so calling code can react appropriately to each.