begin, rescue, ensure

Published by Programming Line in Ruby. Last updated .

Ruby programs don’t have to crash the moment something goes wrong. The begin, rescue, and ensure keywords let you run code that might fail, catch specific problems you anticipate, and guarantee that cleanup code — closing a file, releasing a lock, logging an attempt — runs no matter what happens. Understanding exactly when each clause fires, and how exceptions travel through Ruby’s class hierarchy, is what separates code that merely works from code that fails safely.

Overview: How Exception Handling Works in Ruby

In Ruby, an exception is not a special language construct — it is an ordinary object, an instance of a class that ultimately descends from Exception. When something goes wrong (dividing by zero, calling a method that doesn’t exist, converting an invalid string to a number), Ruby builds one of these exception objects and raises it. Raising immediately halts normal, top-to-bottom execution and starts unwinding the call stack, looking for a place that knows how to handle that particular kind of error. If it reaches the top of the stack with nothing to catch it, Ruby prints a backtrace and the program terminates.

A begin ... end block marks a region of code you want to supervise. Immediately after the protected code, one or more rescue clauses list the exception classes you know how to recover from. Ruby compares the raised exception against each rescue clause in order, using === (true for the exact class or any subclass), and runs the body of the first clause that matches. An optional else clause runs only when the begin body completed without raising anything — and, importantly, an exception raised inside else is not caught by the rescue clauses above it, because those only watch the begin body. An optional ensure clause always runs last, whether an exception was raised, rescued, left unhandled, or the block exited early via return or break — it is Ruby’s guarantee that cleanup code executes.

You rarely need the literal begin keyword: a def, class, or module body already acts as an implicit begin block, so you can attach rescue and ensure directly to a method definition, as several examples below do.

The exception class hierarchy

Every exception class inherits, directly or indirectly, from Exception. A bare rescue (no class named) only catches StandardError and its subclasses — this is deliberate. Serious, low-level conditions like NoMemoryError, SystemExit, or the Interrupt raised when a user presses Ctrl+C descend directly from Exception but not from StandardError, precisely so that ordinary rescue code does not accidentally swallow them.

Class Typical cause
ZeroDivisionError Integer division by zero
NoMethodError Calling a method that doesn’t exist on the receiver (a subclass of NameError)
TypeError An object of the wrong type is passed, e.g. Integer(nil)
ArgumentError Wrong number or format of arguments, e.g. Integer("abc")
IndexError / KeyError Invalid array index / missing hash key via fetch
RuntimeError The default class used by a bare raise "message"
IOError Problems reading or writing streams and files

Syntax

begin
  # code that might raise an exception
rescue SpecificError => e
  # handle SpecificError specifically
rescue AnotherError => e
  # handle AnotherError specifically
else
  # runs only if NO exception was raised
ensure
  # always runs, whether an exception happened or not
end
  • begin — opens the protected block; often omitted because def/class/module already supply an implicit one.
  • rescue ExceptionClass => e — catches that class (and any subclass) and binds the exception object to e; you can stack several, and Ruby tries them top to bottom, using the first match.
  • rescue => e (no class) — shorthand for rescue StandardError => e.
  • else — optional; runs only when the protected code raised nothing at all.
  • ensure — optional; always runs, even after a return, a break, or an exception that is re-raised.
  • end — closes the block.

Inside any rescue clause, calling raise with no arguments re-raises the exception currently being handled (useful for logging before letting it propagate), and calling retry jumps back to the very top of the begin block and runs it again — local variables keep whatever values they already held, which makes it easy to build a bounded retry loop.

Examples

Example 1: Catching a specific exception and guaranteeing cleanup

def divide(a, b)
  begin
    result = a / b
    puts "Result: #{result}"
  rescue ZeroDivisionError => e
    puts "Error: #{e.message}"
  ensure
    puts "Division attempt finished."
  end
end

divide(10, 2)
divide(10, 0)

Output:

Result: 5
Division attempt finished.
Error: divided by 0
Division attempt finished.

The first call divides cleanly, so the rescue is skipped and only ensure runs afterward. The second call divides by zero, which raises ZeroDivisionError; the rescue clause matches and prints the error’s message, and then ensure still runs — notice it fires exactly once per call, whether or not an exception occurred.

Example 2: Multiple rescue clauses on an implicit begin block

def parse_number(str)
  Integer(str)
rescue ArgumentError => e
  puts "Invalid number: #{str.inspect} (#{e.message})"
  nil
rescue TypeError => e
  puts "Wrong type: #{e.message}"
  nil
end

puts parse_number("42").inspect
puts parse_number("abc").inspect
puts parse_number(nil).inspect

Output:

42
Invalid number: "abc" (invalid value for Integer(): "abc")
nil
Wrong type: can't convert nil into Integer
nil

This method has no literal begin at all — the def itself is the protected block. Integer("42") succeeds and the method implicitly returns 42. Integer("abc") raises ArgumentError, matching the first rescue. Integer(nil) raises TypeError instead (nil can’t be converted at all), so Ruby skips the first rescue clause — whose class doesn’t match — and falls through to the second one. Each rescue branch returns nil as the method’s value, which is why the outer puts ... .inspect prints nil after the warning.

Example 3: A custom exception, with ensure surviving an early return

class InsufficientFundsError < StandardError
  def initialize(msg = "Not enough funds in the account")
    super
  end
end

def withdraw(balance, amount)
  begin
    raise InsufficientFundsError if amount > balance
    balance - amount
  rescue InsufficientFundsError => e
    puts "Transaction failed: #{e.message}"
    return balance
  ensure
    puts "Withdrawal attempt logged."
  end
end

puts withdraw(100, 30)
puts withdraw(100, 150)

Output:

Withdrawal attempt logged.
70
Transaction failed: Not enough funds in the account
Withdrawal attempt logged.
100

InsufficientFundsError is a custom exception class that inherits from StandardError (never from bare Exception — see Best Practices) and supplies a default message. The first withdrawal succeeds, so ensure runs right after the begin body finishes. The second withdrawal raises the custom error; the rescue clause prints a message and then calls return balance to bail out of the method early — but ensure still runs before that return actually hands control back to the caller. This is the core guarantee of ensure: it runs even when you leave the block via return, break, or an unrescued exception.

How It Works Step by Step

When Ruby executes a begin (or an implicit method-level one), it follows this order:

  • Run the statements in the begin body, top to bottom.
  • If a statement raises, execution jumps immediately to the rescue clauses — the rest of the begin body is skipped entirely.
  • Ruby checks each rescue clause in the order written, using === against the raised exception’s class; the first match wins and its body runs.
  • If no rescue clause matches, the exception keeps propagating up the call stack (past this begin block) looking for a match elsewhere.
  • If nothing raised at all, an else clause (if present) runs after the begin body.
  • Regardless of any of the above, the ensure clause runs last, right before control actually leaves the block.
  • If a rescue clause calls retry, Ruby discards the ensure-pending exit and jumps back to step 1, re-running the begin body from the top with local variables intact.
def fetch_with_retry
  attempts_left = 3

  begin
    attempts_left -= 1
    raise "Temporary network glitch" if attempts_left > 0
    "Data fetched successfully"
  rescue RuntimeError => e
    puts "Attempt failed (#{e.message}), #{attempts_left} attempts left."
    retry if attempts_left > 0
    "Giving up after retries"
  end
end

puts fetch_with_retry

Output:

Attempt failed (Temporary network glitch), 2 attempts left.
Attempt failed (Temporary network glitch), 1 attempts left.
Data fetched successfully

attempts_left is declared once, outside the begin block, so retry doesn’t reset it — each pass decrements it further. The first two passes raise on purpose to simulate a flaky operation; by the third pass attempts_left is 0, the raise condition is false, and the begin body’s last expression, "Data fetched successfully", becomes the method’s return value without ever reaching the rescue clause again. Had all three attempts failed, the rescue body’s own last line, "Giving up after retries", would have been returned instead. Always cap retries like this — an unconditional retry can loop forever.

Common Mistakes

Mistake 1: Rescuing bare Exception instead of the default

It’s tempting to write rescue Exception => e to “catch everything,” but Exception is the root of the entire hierarchy, including things that should almost never be silently caught, like SystemExit and Interrupt (Ctrl+C). Catching those means your program can’t be cleanly stopped and can hide serious VM-level problems.

def risky_operation
  raise "Something went wrong"
rescue Exception => e
  puts "Rescued: #{e.class}: #{e.message}"
end

risky_operation

Output:

Rescued: RuntimeError: Something went wrong

For an ordinary RuntimeError this looks harmless, but the same handler would also swallow a user’s Ctrl+C. Use a bare rescue (or rescue StandardError) instead, which produces identical behavior for expected application errors while letting system-level signals pass through untouched:

def risky_operation
  raise "Something went wrong"
rescue => e
  puts "Rescued: #{e.class}: #{e.message}"
end

risky_operation

Output:

Rescued: RuntimeError: Something went wrong

Mistake 2: Using the inline rescue modifier to hide bugs

The one-line expression rescue fallback form catches any StandardError, which means it will just as happily swallow a typo that raises NoMethodError as it will a genuinely missing value — you lose the ability to tell them apart.

def find_user_age(users, name)
  users[name].age rescue nil
end

users = { "Ada" => Struct.new(:age).new(36) }
puts find_user_age(users, "Ada")
puts find_user_age(users, "Grace").inspect

Output:

36
nil

This looks fine here, but the same rescue nil would just as silently hide a real bug, such as calling .ages instead of .age by mistake — you’d get nil back with no clue why. Check explicitly for the condition you actually expect instead:

def find_user_age(users, name)
  user = users[name]
  return nil unless user

  user.age
end

users = { "Ada" => Struct.new(:age).new(36) }
puts find_user_age(users, "Ada")
puts find_user_age(users, "Grace").inspect

Output:

36
nil

The output is identical for these inputs, but now a genuine typo like user.ages would raise NoMethodError loudly instead of being swallowed.

Best Practices

  • Rescue the most specific exception class you can, not bare Exception — let unexpected, system-level conditions propagate.
  • Keep the begin body as small as possible; only wrap the lines that can actually raise, so you don’t accidentally rescue an error from unrelated code.
  • Define custom errors as subclasses of StandardError, never Exception, so they’re caught by ordinary bare rescue clauses.
  • Avoid the one-line rescue modifier in production code; it silently catches every StandardError, including ones you didn’t anticipate.
  • Don’t use exceptions for ordinary control flow (like “value not found”); reserve them for truly exceptional situations, and use return values or nil checks otherwise.
  • Always bound a retry loop with a counter or condition — an unconditional retry can spin forever.
  • Use ensure for side effects only (closing files, releasing locks); its own return value is discarded unless it explicitly overrides one with return, which is rarely a good idea.
  • Log or re-raise an exception you catch but can’t fully handle — don’t let a rescue clause quietly discard information you’ll need later.

Practice Exercises

  • Write a method safe_sqrt(n) that calls Math.sqrt(n) and rescues Math::DomainError (raised for negative input), returning nil in that case instead of crashing. Test it with 16 and -4; you should get 4.0 and nil.
  • Write a method read_setting(hash, key) that uses begin/rescue/else/ensure: fetch the value with hash.fetch(key), rescue KeyError to print a warning and return "unknown", use else to print "Loaded #{key}" only when the key was found, and use ensure to print "Lookup finished." every time. Try it with a hash that has :name but not :age.
  • Write a method that simulates logging in using a counter and retry: raise a custom LoginError on the first two attempts and succeed on the third, printing a message on each attempt. Cap it at 3 tries total so it can never loop forever.

Summary

  • begin ... rescue ... else ... ensure ... end supervises code that might fail; def/class/module already provide an implicit begin.
  • A raised exception is matched against rescue clauses top to bottom using ===, so subclasses match a parent class’s rescue.
  • A bare rescue only catches StandardError and its subclasses — not Exception-level conditions like Interrupt or SystemExit.
  • else runs only when nothing was raised; ensure always runs, even through a return, break, or unhandled exception.
  • retry jumps back to the top of the begin block, keeping local variable state, so always pair it with a bound to avoid infinite loops.
  • Custom exceptions should inherit from StandardError, and the inline rescue modifier should be avoided outside of quick scripts because it hides bugs.