Raising Exceptions

Published by Programming Line in Ruby. Last updated .

Sometimes the correct response to bad input, an invalid state, or a broken assumption isn’t to keep going — it’s to stop immediately and say exactly what went wrong. In Ruby, you do this by raising an exception: an object that represents an error, carries a message and a backtrace, and unwinds the call stack until something rescues it (or the program crashes with a report). Knowing how to raise exceptions well — with the right class, a useful message, and the right amount of detail — is what separates code that fails loudly and clearly from code that fails in confusing, hard-to-debug ways.

Overview: How Raising Exceptions Works

In Ruby, exceptions are just objects — instances of Exception or one of its subclasses. When you call raise, Ruby creates (or reuses) an exception object and throws it up the call stack. Ruby stops executing the current method immediately, unwinds through each enclosing method call, and looks for a matching rescue clause. If it finds one, execution jumps there. If it never finds one, the program terminates and prints the exception’s class, message, and backtrace to stderr.

Every exception you raise descends from a small hierarchy. At the top is Exception itself. Its direct children include things like NoMemoryError, SystemExit, and — the one that matters for almost everything you write — StandardError. StandardError is the ancestor of the exceptions you deal with day to day: RuntimeError, ArgumentError, TypeError, ZeroDivisionError, KeyError, IndexError, and so on. A bare rescue (with no class named) only catches StandardError and its descendants — it deliberately does not catch things like SystemExit or Interrupt, because those represent “the program is being asked to stop,” not “something broke.” This is why your own exception classes should almost always inherit from StandardError, not from Exception directly.

When you write raise "something broke", Ruby doesn’t raise a plain string — it constructs a RuntimeError with that string as its message. When you write raise ArgumentError, "bad input", Ruby calls ArgumentError.exception("bad input"), which under the hood calls ArgumentError.new("bad input"). That’s why you can also pass an already-built exception instance directly: raise MyError.new(...). All three forms end up doing the same fundamental thing — producing an exception object and throwing it.

Syntax

raise
raise "message"
raise ExceptionClass
raise ExceptionClass, "message"
raise ExceptionClass.new("message", extra: "data")
  • raise with no arguments, used inside a rescue block, re-raises the exception currently being handled (preserving its original backtrace). Used outside a rescue block, it raises a generic RuntimeError with the message "unhandled exception".
  • raise "message" raises a RuntimeError with that message — a quick way to fail, but too vague for library code since callers can’t easily distinguish it from other RuntimeErrors.
  • raise ExceptionClass raises a new instance of that class with its default message (usually the class name).
  • raise ExceptionClass, "message" is the most common form: it raises a new instance of the class with a specific, helpful message.
  • raise ExceptionClass.new(...) lets you build the exception yourself first, which is required when your custom exception’s initialize takes extra arguments beyond a plain message.

There’s also an older alias, fail, which behaves identically to raise. Modern Ruby style favors raise everywhere for consistency, so this lesson (and most current style guides) uses raise exclusively.

Examples

Example 1: Raising built-in exceptions with guard clauses

def withdraw(balance, amount)
  raise ArgumentError, "amount must be positive" if amount <= 0
  raise "insufficient funds" if amount > balance

  balance - amount
end

begin
  puts withdraw(100, 150)
rescue ArgumentError => e
  puts "Argument error: #{e.message}"
rescue RuntimeError => e
  puts "Runtime error: #{e.message}"
end
Runtime error: insufficient funds

The method uses two guard clauses at the top — a common Ruby idiom — to reject invalid states before doing any real work. Here amount (150) is positive, so the first raise is skipped, but it’s greater than balance (100), so Ruby raises a RuntimeError with the message "insufficient funds". That exception unwinds out of withdraw and is caught by the second rescue clause, which prints the message. Notice the order of the rescue clauses matters: Ruby checks them top to bottom and uses the first one whose class matches (via ===), so more specific exception classes should generally come before more general ones.

Example 2: Raising a custom exception class

class InsufficientFundsError < StandardError
  attr_reader :balance, :amount

  def initialize(balance:, amount:)
    @balance = balance
    @amount = amount
    super("cannot withdraw #{amount} from balance of #{balance}")
  end
end

def withdraw(balance, amount)
  raise InsufficientFundsError.new(balance: balance, amount: amount) if amount > balance

  balance - amount
end

begin
  withdraw(50, 75)
rescue InsufficientFundsError => e
  puts e.message
  puts "Short by #{e.amount - e.balance}"
end
cannot withdraw 75 from balance of 50
Short by 25

Custom exception classes let you attach structured data to a failure, not just a string. InsufficientFundsError inherits from StandardError and stores balance and amount as readable attributes. Its initialize calls super(...) with a formatted message, which hands that string to Exception#initialize so e.message works normally. Because we need to pass keyword arguments, we can’t use the short raise ClassName, "text" form — instead we build the instance ourselves with .new and raise that. The rescue block then reads both e.message and the custom e.amount/e.balance attributes to compute how short the withdrawal was.

Example 3: Re-raising a different exception with a preserved cause

class ConfigError < StandardError; end

def load_setting(settings, key)
  settings.fetch(key)
rescue KeyError => e
  raise ConfigError, "missing config key: #{key}"
end

begin
  load_setting({ host: "localhost" }, :port)
rescue ConfigError => e
  puts "Failed: #{e.message}"
  puts "Caused by: #{e.cause.class}"
end
Failed: missing config key: port
Caused by: KeyError

Hash#fetch raises KeyError when the key isn’t present, and :port isn’t in { host: "localhost" }. Rather than let a low-level KeyError leak out of load_setting (which would force every caller to know about hash internals), the method rescues it and raises a more meaningful ConfigError instead. Because this new raise happens while Ruby is already handling KeyError, Ruby automatically links the two: the new exception’s cause is set to the original KeyError. This is exception chaining — you get a clean, domain-specific message for callers while the original low-level cause is still available for debugging via e.cause.

How It Works Step by Step

  • Ruby evaluates the raise call’s arguments and produces an exception object (creating one from a class/message, or using an instance you already built).
  • Ruby captures a backtrace — the chain of method calls that led to this point — and attaches it to the exception.
  • If this raise happens inside an active rescue block, Ruby records the exception currently being handled as the new exception’s cause.
  • Execution immediately stops at the point of the raise; no code after it in that method runs.
  • Ruby unwinds the call stack one frame at a time, running any ensure blocks it passes through along the way, searching each enclosing begin/rescue (or method-level rescue) for a class that matches via ===.
  • The first matching rescue clause runs with the exception bound to its variable (=> e); execution then continues after the whole begin/rescue/end, not back where the raise happened.
  • If no matching rescue is found anywhere up the stack, the program terminates and Ruby prints the exception’s class, message, and backtrace to stderr.

Common Mistakes

Mistake 1: Overriding initialize without calling super

class ConfigError < StandardError
  def initialize(key)
    @key = key
  end
end

begin
  raise ConfigError, "port"
rescue ConfigError => e
  puts e.message
end
ConfigError

You’d expect e.message to be "port", but it prints "ConfigError" instead. Because initialize never calls super, the message is never handed to Exception#initialize, so Exception falls back to using the class name as the message. Always call super with the message you want to expose:

class ConfigError < StandardError
  def initialize(key)
    @key = key
    super("invalid config key: #{key}")
  end
end

begin
  raise ConfigError, "port"
rescue ConfigError => e
  puts e.message
end
invalid config key: port

Mistake 2: Rescuing (or implying) Exception instead of StandardError

begin
  raise "boom"
rescue Exception => e
  puts "rescued: #{e.message}"
end
rescued: boom

This runs and looks harmless, but rescue Exception is a trap: Exception sits above StandardError in the hierarchy and also covers things like SystemExit (raised by exit) and Interrupt (raised by Ctrl-C) and NoMemoryError. Rescuing Exception means your rescue will silently swallow a user’s Ctrl-C or block a clean exit, which is almost never what you want. Prefer a bare rescue (equivalent to rescue StandardError) or name StandardError explicitly:

begin
  raise "boom"
rescue StandardError => e
  puts "rescued: #{e.message}"
end
rescued: boom

Best Practices

  • Define custom exceptions as subclasses of StandardError (or a more specific existing exception), never of Exception directly.
  • Give every raised exception a message that says what went wrong and, where useful, what value caused it — future-you (or a teammate) will read that message in a log, not the source code.
  • Raise the most specific built-in exception that fits (ArgumentError, TypeError, KeyError) before reaching for a bare RuntimeError or a custom class.
  • When you rescue a low-level exception and raise a more meaningful one, let Ruby’s automatic cause chaining preserve the original error for debugging — don’t discard it.
  • Use guard clauses (raise ... if invalid?) at the top of a method to fail fast, rather than deeply nesting the “happy path” logic inside conditionals.
  • Don’t use exceptions for ordinary, expected control flow (like “user not found” in a normal lookup) — reserve them for genuinely exceptional situations; consider returning nil or a result object for expected “not found” cases instead.
  • Remember Ruby’s truthiness rule when writing guard conditions: only nil and false are falsy, so checks like raise "empty" if value when value is 0 or "" will not behave the way a beginner might expect — those are truthy.

Practice Exercises

  • Write a method divide(a, b) that raises ZeroDivisionError with the message "cannot divide by zero" when b is zero, and otherwise returns a / b. Rescue it and print the message.
  • Define a custom exception class ValidationError < StandardError that takes a keyword argument field: and builds a message like "field is required". Write a method validate(name) that raises it when name is nil or an empty string, then rescue it and print both e.message and e.field.
  • Write a method that rescues a KeyError from a hash lookup and re-raises it as a custom MissingDataError. Rescue the outer exception and print e.cause.class to confirm the original KeyError was preserved as the cause.

Summary

  • raise creates an exception object and immediately unwinds the call stack looking for a matching rescue.
  • raise "text" raises a RuntimeError; raise SomeError, "text" raises a specific class with a message; raise SomeError.new(...) is needed for exceptions with custom constructors.
  • Bare raise inside a rescue block re-raises the current exception, preserving its backtrace.
  • Custom exception classes should inherit from StandardError and must call super(message) in their initialize to set the message correctly.
  • Raising a new exception while handling another automatically sets cause, chaining the original error for debugging without losing it.
  • Rescue StandardError (or use a bare rescue), not Exception, so you don’t accidentally swallow signals like SystemExit and Interrupt.