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

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  nil

2️⃣ 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
end

Your 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 zero

Mini-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 here

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Exceptions

ConceptRuby Syntax
Handlebegin ... rescue ... end
Capture errorrescue => e
Always runensure ... end
Throw errorraise "msg"
Custom errorclass E < StandardError
Retry blockretry

🎉 Lesson Complete!

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

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.