---
title: Exceptions
source: 'https://academia.sh/en/courses/ruby-fundamentals/exceptions'
course: 'Ruby Fundamentals'
language: en
updated: '2026-10-21T05:03:43+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Catches an exception with rescue and reads its type with e.class'
    - 'Derives its own exception class from StandardError and raises it with raise'
    - 'Writes resource cleanup with ensure and retrying with retry'
    - 'Measures that the rescue construct is syntax and raise is a message'
---

# Exceptions

The rescue, ensure and retry constructs are measured as the language's own syntax, raise as a message, and the match in a rescue branch as being made with a message to a class.

At the end of the previous lesson, a lambda called with the wrong number of arguments raised `ArgumentError`, and that exception was caught with `rescue`. This lesson makes that construct its subject. An **exception** is an event that interrupts the normal flow; **raising** produces it and **rescuing** receives it. M12/K01 measured the error channels in PHP; the question of an error channel is not asked here, only who answers the construct is measured.

This lesson's responder is **syntax**: `rescue` is not a method but a construct of the language itself.

## rescue Is an Expression

The core observation rests on the `defined?` question: what does `defined?` say about a `begin ... rescue ... end` block? If it were a method call, `"method"` would come back.

```ruby
# reply.rb — helpers that classify who answers a message

# Classification asks in order: is the expression a message at all (defined?),
# then do two different blocks give two different results, then who owns the method.
def responder(definition, owner: nil, result: nil, with_blocks: nil)
  return ["syntax", "defined? #{definition}"] unless definition == "method"

  if with_blocks
    first, second = with_blocks.call
    return ["block", "#{first.inspect} / #{second.inspect}"] unless first == second
  end

  kind = owner.instance_of?(Class) ? "class" : "module"
  [kind, "#{owner}, result #{result.inspect}"]
end
```

```ruby
# rescue.rb — who answers the rescue construct
require_relative "reply"

definition = defined?(begin; Integer("x"); rescue ArgumentError; 0; end)
kind, evidence = responder(definition)
puts "#{kind}  #{evidence}"
value = Integer("x") rescue 0
puts value
puts Integer("x", exception: false).inspect
```
```
syntax  defined? expression
0
nil
```

The first line is the core's `rescue` observation: the **responder** is **syntax**, and `defined?` gave `"expression"`. The second line shows the statement modifier form: `Integer("x") rescue 0` caught the exception and gave `0`. The third line did the same job with the `exception: false` keyword argument, without raising an exception, and returned `nil`. **CB15.** This lesson measures the modifier `rescue` only with `ArgumentError`; whether it catches other exception classes is not measured.

On the second line, `Integer("x") rescue 0` was first assigned to a variable and then printed; the modifier form was not written directly inside a method argument or inside `defined?`. How it resolves in those positions is not measured in this lesson.

## Exception Classes

Every exception is an instance of a class. `e.class` gives its type; the class name is used instead of the message (`e.message`), because the message text can change between versions. The lesson's own `raise` text is outside this rule.

```ruby
# classes.rb — exception classes
def attempt
  yield
rescue => e
  e.class
end

puts attempt { 1 / 0 }
puts attempt { raise "x" }
puts attempt { Integer("x") }
puts 1.0 / 0

class OutOfStock < StandardError; end

begin
  raise OutOfStock, "out of stock"
rescue OutOfStock => e
  puts "#{e.class} #{e.message}"
end
```
```
ZeroDivisionError
RuntimeError
ArgumentError
Infinity
OutOfStock out of stock
```

Integer division by zero raised `ZeroDivisionError`; float division gave `Infinity` instead of an exception. A plain `raise "x"` produced a `RuntimeError`. Our own class `OutOfStock` derived from `StandardError`, and `rescue OutOfStock => e` caught only it. The message could be printed here because it is the lesson's own `raise` text.

## ensure and retry

The `ensure` branch runs whether or not there is an exception; it is used for resource cleanup. `retry` runs the `begin` block again from the start.

```ruby
# retry.rb — ensure and retry
attempts = 0
begin
  attempts += 1
  raise "not yet" if attempts < 3
  puts "succeeded #{attempts}"
rescue
  retry
end

events = []
begin
  events << 1
  raise "error"
rescue
  events << :rescued
ensure
  events << 2
end
puts events.inspect
```
```
succeeded 3
[1, :rescued, 2]
```

On every failed attempt, `retry` restarted the block, and the job finished on the third attempt. In the second block, the `ensure` branch ran even after the exception was caught. **CB16.** The retry count is the lesson's own counter; in a real retry scenario an upper bound is needed to prevent an endless loop, and that is not measured here.

## Branches and else

A `begin` block can carry several `rescue` branches; the exception goes to the first branch that matches. The `else` branch runs only when no exception occurred. A class's parent is asked with `superclass`.

```ruby
# branches.rb — rescue branches and else
def classify(input)
  Integer(input)
rescue ArgumentError
  "invalid"
rescue TypeError
  "type"
else
  "ok"
end

puts classify("5")
puts classify("x")
puts classify(nil)
puts ZeroDivisionError.superclass
puts StandardError.superclass
```
```
ok
invalid
type
StandardError
Exception
```

Because `"5"` raised no exception, the `else` branch ran. `"x"` went to the `ArgumentError` branch and `nil` to the `TypeError` branch: the first matching branch won. The parent of `ZeroDivisionError` turned out to be `StandardError`, and the parent of that is `Exception`; a bare `rescue` therefore catches `ZeroDivisionError`. **CB18.** The order of the branches matters: if a broad class is written first, the branches after it never run, but that behavior is not measured here.

## StandardError and Exception

A `rescue` with no parameter and `rescue => e` catch only `StandardError` and its subclasses. Exceptions that stand outside `StandardError` in the hierarchy pass through this catcher. The measurement below shows that an `ArgumentError` is caught, that a `NotImplementedError` is not caught, and the relevant parent classes.

```ruby
# hierarchy.rb — which exceptions does rescue catch
def guarded
  yield
  "none"
rescue => e
  "caught #{e.class}"
end

puts guarded { raise ArgumentError }
begin
  guarded { raise NotImplementedError }
rescue NotImplementedError => e
  puts "outside #{e.class}"
end
puts NotImplementedError.superclass
puts ScriptError.superclass
puts KeyError.superclass
puts IndexError.superclass
```
```
caught ArgumentError
outside NotImplementedError
ScriptError
Exception
IndexError
StandardError
```

`ArgumentError` was caught and `caught ArgumentError` was printed. `NotImplementedError` passed through the `rescue` of `guarded` and was caught in the outer `begin` block. The parent chain explains this: `NotImplementedError` is under `ScriptError`, which is under `Exception`; it does not reach `StandardError`. `KeyError`, on the other hand, connects to `StandardError` through `IndexError`. **CB34.** Only the parent chain of these four classes was printed.

## ensure Does Not Change the Return Value

The `ensure` branch runs however the method ends. However, the last expression of this branch does not become the method's return value. `ensure` runs on an early exit with `return` too.

```ruby
# ensure_value.rb — the ensure branch and the return value
def value
  1
ensure
  2
end

def early
  return 1
ensure
  puts "cleanup"
end

puts value
puts early
```
```
1
cleanup
1
```

`value` returned `1`; the `2` inside `ensure` did not change the return value. `early` exited with `return 1`, but `ensure` first printed the `cleanup` line and then `1` was returned. **CB35.** Only these two forms were measured; writing a `return` inside `ensure` was not measured.

## Re-raising

A bare `raise` inside `rescue` re-raises the caught exception as it is. This way a layer records the event but leaves the decision to the layer above.

```ruby
# reraise.rb — re-raising with raise
def outer
  yield
rescue ArgumentError => e
  puts "logged #{e.class}"
  raise
end

begin
  outer { Integer("x") }
rescue => e
  puts "again #{e.class}"
end
```
```
logged ArgumentError
again ArgumentError
```

The inner `rescue` first printed the `logged ArgumentError` line and raised the same exception with a bare `raise`. The outer `rescue` saw the same class: `again ArgumentError`. **CB36.** The exception message was not printed; only the class was measured.

## The retry Limit and the Cause Chain

`retry` runs the block from the start; left unbounded, it falls into an endless loop. That is why it is always written together with a counter. If a new exception is raised inside a `rescue`, the earlier exception remains on the new one through `cause`.

```ruby
# limit.rb — a retry counter and cause
attempts = 0
begin
  attempts += 1
  raise "x" if attempts < 10
rescue
  retry if attempts < 3
  puts "gave up #{attempts}"
end

begin
  begin
    Integer("x")
  rescue => e
    raise RuntimeError, "upper"
  end
rescue => upper
  puts upper.cause.class
end
```
```
gave up 3
ArgumentError
```

In the first two rounds `retry` ran the block again; in the third round the condition was not met, so `gave up 3` was printed. In the second part, `Integer("x")` raised an `ArgumentError`, a new `RuntimeError` was raised inside the `rescue`, and the outer `upper.cause.class` gave `ArgumentError`. **CB37.** `retry` was measured only in the `begin` and `rescue` form.

## Multiple Classes and Branch Order

A `rescue` can list several classes separated by commas. If there are several `rescue` branches, the first matching branch runs and the later ones are not looked at.

```ruby
# order.rb — a rescue list and branch order
def classify
  yield
rescue ZeroDivisionError, TypeError
  "both"
rescue StandardError
  "general"
end
puts classify { 1 / 0 }
puts classify { 1 + nil }
puts classify { Integer("x") }
def reversed
  yield
rescue StandardError
  "general"
rescue ZeroDivisionError
  "specific"
end
puts reversed { 1 / 0 }
```
```
both
both
general
general
```

`rescue ZeroDivisionError, TypeError` caught both `1 / 0` and `1 + nil`: both printed `both`. `Integer("x")` was not in this list and fell to the `general` branch. In the `reversed` method, `StandardError` was written first; `1 / 0` got caught by that branch and the `specific` branch was never reached. The specific class must be written before the general one. **CB48.** The measurement was made with these three exception classes.

## The Order of begin, else and ensure

The branches in a `begin` block run in a particular order. The order in which the branches run is read from the symbols appended to an array.

```ruby
# sequence.rb — the running order of the branches
steps = []
begin
  steps << :body
rescue
  steps << :rescue
else
  steps << :else
ensure
  steps << :ensure
end
puts steps.inspect
steps = []
begin
  steps << :body
  raise "x"
rescue
  steps << :rescue
else
  steps << :else
ensure
  steps << :ensure
end
puts steps.inspect
```
```
[:body, :else, :ensure]
[:body, :rescue, :ensure]
```

With no error, `else` ran after the body and `ensure` ran last. When an error was raised, `rescue` came into play, `else` was skipped and `ensure` ran again. `ensure` took the last place in both cases. **CB49.**

## An Exception Class with a Field

Your own exception class can carry extra information. You store the field in `initialize` and pass the message with `super`.

```ruby
# with_field.rb — an exception class with its own field
class StockError < StandardError
  attr_reader :quantity

  def initialize(quantity)
    @quantity = quantity
    super("insufficient stock")
  end
end

begin
  raise StockError.new(3)
rescue StockError => e
  puts "#{e.class} quantity=#{e.quantity} message=#{e.message}"
end
puts StockError.superclass
puts StockError.instance_method(:quantity).owner
```
```
StockError quantity=3 message=insufficient stock
StandardError
StockError
```

`StockError` derived from `StandardError`, carried the `quantity` field and gave its own message text with `super`. Because `rescue` reached the field with `e.quantity`, `quantity=3` was printed; `message=insufficient stock` is text the lesson wrote itself. The owner of the `quantity` reader is `StockError`: the class answers this method. **CB50.**

## catch and throw

`catch` and `throw` provide a labeled exit. `throw` climbs out to the nearest `catch` block opened with the same label and carries a value to it.

```ruby
# tags.rb — catch and throw
r = catch(:done) do
  [1, 2, 3].each { |n| throw :done, n * 10 if n == 2 }
  :nothing
end
puts r
puts "owner=#{method(:throw).owner}"
```
```
20
owner=Kernel
```

`throw :done, 20` ended the inner `each` block and the outer `catch` block; `catch` returned `20` instead of `:nothing`. The owner of `throw` was printed as `Kernel`. **CB51.** Only this single-label example was measured.

## Nested ensure and the rescue Suffix

If an inner `begin` block has an `ensure` and the exception is not caught there, `ensure` runs before the exception goes out. As a short form, `expression rescue value` uses the value if the expression fails; this form can be assigned to a variable in parentheses.

```ruby
# nested.rb — nested ensure and the rescue suffix
log = []
begin
  begin
    log << :inner
    raise ArgumentError
  ensure
    log << :cleanup
  end
rescue ArgumentError
  log << :outer
end
puts log.inspect
recovered = (Integer("x") rescue -1)
puts recovered
```
```
[:inner, :cleanup, :outer]
-1
```

The exception was not caught in the inner block, but `ensure` ran before it went out, and the order became `[:inner, :cleanup, :outer]`. So `ensure` runs before the outer `rescue`. When `Integer("x")` failed, the `-1` in the suffix form was used. **CB55.** That the suffix form catches only the `StandardError` family was not measured; only this single example was tried in this lesson.

## The Bounding Measurement

`raise` is a message, that is, a method call; `rescue` is syntax. The two have different responders. One can also ask whether the `rescue ArgumentError => e` branch makes a match of its own: does the branch ask the `ArgumentError` class with a message?

```ruby
# matching.rb — the match of a rescue branch
puts "raise owner=#{method(:raise).owner}"

begin
  Integer("x")
rescue ArgumentError => e
  puts ArgumentError === e
  puts "=== owner=#{ArgumentError.method(:===).owner}"
end
```
```
raise owner=Kernel
true
=== owner=Module
```

`raise` is a method and its owner is the `Kernel` module. The branch's match is made with the `===` message: `ArgumentError === e` gave `true`, and the owner of `===` is `Module`, not `Class`; that is, the class takes its own `===` from a higher structure. This sharpens the distinction: the language resolves the `rescue` keyword, but which branch receives the exception is decided by the class's `===` answer. The language builds the construct and the class decides the match. The class written on the same `rescue` line is therefore an ordinary class object and answers its own match message. **CB17.** This measurement was made only for the `ArgumentError` branch; that different exception classes get the same message from the same owner is not assumed.

The result agrees with the core's classification: `defined?` named the expression `"expression"`, that is, the `rescue` keyword is not a message; the branch match, as seen above, is made with the `===` message. The raising side is different; `raise` is an ordinary method, it comes from the `Kernel` module and takes an exception class or text as an argument. So in an exception event two responders stand side by side: a module answers the raising and the language itself answers the rescuing. This is also the reason **syntax** was chosen as the lesson's responder: the question measured is who builds the rescuing construct.

## Summary

- An exception is an event that interrupts the normal flow; `rescue` catches, `raise` raises, `ensure` runs in every case, and `retry` restarts the block.
- This lesson's responder is **syntax**: `defined?` returns `"expression"` for `begin ... rescue ... end`.
- The statement modifier `rescue` and `exception: false` do the same job in two ways: a value instead of an exception.
- `raise` is a method and its owner is the `Kernel` module; the match of a `rescue` branch is made with the `===` message.
- The bounding measurement: `rescue` is syntax, but the match inside its branch is a message to a class.

## Next Step

In this lesson, all the operations that raise exceptions ran in memory. The last lesson brings files and input/output: whether the thing that opens a file is a block or a method, and which construct `ensure` cleans up in this job.
