Raising Exceptions
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")
raisewith no arguments, used inside arescueblock, re-raises the exception currently being handled (preserving its original backtrace). Used outside arescueblock, it raises a genericRuntimeErrorwith the message"unhandled exception".raise "message"raises aRuntimeErrorwith that message — a quick way to fail, but too vague for library code since callers can’t easily distinguish it from otherRuntimeErrors.raise ExceptionClassraises 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’sinitializetakes 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
raisecall’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
raisehappens inside an activerescueblock, Ruby records the exception currently being handled as the new exception’scause. - 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
ensureblocks it passes through along the way, searching each enclosingbegin/rescue(or method-levelrescue) for a class that matches via===. - The first matching
rescueclause runs with the exception bound to its variable (=> e); execution then continues after the wholebegin/rescue/end, not back where the raise happened. - If no matching
rescueis found anywhere up the stack, the program terminates and Ruby prints the exception’s class, message, and backtrace tostderr.
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 ofExceptiondirectly. - 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 bareRuntimeErroror a custom class. - When you rescue a low-level exception and raise a more meaningful one, let Ruby’s automatic
causechaining 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
nilor a result object for expected “not found” cases instead. - Remember Ruby’s truthiness rule when writing guard conditions: only
nilandfalseare falsy, so checks likeraise "empty" if valuewhenvalueis0or""will not behave the way a beginner might expect — those are truthy.
Practice Exercises
- Write a method
divide(a, b)that raisesZeroDivisionErrorwith the message"cannot divide by zero"whenbis zero, and otherwise returnsa / b. Rescue it and print the message. - Define a custom exception class
ValidationError < StandardErrorthat takes a keyword argumentfield:and builds a message like"field is required". Write a methodvalidate(name)that raises it whennameisnilor an empty string, then rescue it and print bothe.messageande.field. - Write a method that rescues a
KeyErrorfrom a hash lookup and re-raises it as a customMissingDataError. Rescue the outer exception and printe.cause.classto confirm the originalKeyErrorwas preserved as the cause.
Summary
raisecreates an exception object and immediately unwinds the call stack looking for a matchingrescue.raise "text"raises aRuntimeError;raise SomeError, "text"raises a specific class with a message;raise SomeError.new(...)is needed for exceptions with custom constructors.- Bare
raiseinside arescueblock re-raises the current exception, preserving its backtrace. - Custom exception classes should inherit from
StandardErrorand must callsuper(message)in theirinitializeto 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 barerescue), notException, so you don’t accidentally swallow signals likeSystemExitandInterrupt.
