APM Academy

Rescuing Exceptions in Ruby: raise, rescue & retry

Rescuing Exceptions in Ruby: raise, rescue & retry

In Ruby, rescue catches a raised exception before it crashes your program: wrap risky code in a begin/rescue block, name the exception class you expect, and handle the failure. A bare rescue catches StandardError and its subclasses — not Exception, which also covers process signals like Interrupt. Raising with a message alone — raise "boom" — creates a RuntimeError.

An unhandled exception travels up the call stack until it reaches the top and crashes the process. This guide follows that journey from both ends: how exceptions are raised, where Ruby’s exception hierarchy fits in, how to rescue at the right level, the mistakes that swallow real bugs, and the patterns — re-raising, ensure, retry — that keep failures visible. Every example and error message was verified on Ruby 3.4.

Raising Exceptions in Ruby

The quickest way to meet an exception is to raise one yourself. raise with a string creates a RuntimeError that carries the string as its message:

Ruby
raise "boom"
Shell
$ ruby r.rb
r.rb:1:in '<main>': boom (RuntimeError)

Because nothing rescues the exception, it crashes the program: Ruby prints the location, the message, and the exception class, and the process exits with a non-zero status.

To raise a different class, pass it before the message:

Ruby
raise ArgumentError, "argument is not a filename"
Shell
$ ruby r.rb
r.rb:1:in '<main>': argument is not a filename (ArgumentError)

raise ArgumentError, "argument is not a filename"
      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The caret lines come from error_highlight, a default gem in Ruby 3.4 that underlines the expression responsible for the exception.

Passing an exception instance works the same way — raise ArgumentError.new("argument is not a filename") is equivalent to the class-and-message form. Instances become useful when your exception class takes extra arguments, like a response object or a record ID.

raise on its own, with no arguments, has two behaviors. Outside of a rescue clause, it raises a RuntimeError with the message unhandled exception. Inside a rescue clause, it re-raises the exception currently being handled — the same object, unchanged — which makes it the backbone of the report-then-re-raise pattern covered in the Re-Raising Exceptions section.

That’s the complete menu: an exception class, an exception instance, or a string. Raising anything else — raise 42, or a class that doesn’t inherit from Exception — fails with a TypeError: exception class/object expected. That error usually appears while defining your own error classes, and the custom exceptions guide breaks down its causes and fixes.

Rescuing Exceptions with begin/rescue

To handle an exception instead of crashing, wrap the risky code in a begin/rescue block and name the class you want to catch. The => e part assigns the exception object to a variable, so the handler can read its message and class:

Ruby
begin
  raise "This exception will be rescued!"
rescue StandardError => e
  puts "Rescued: #{e.inspect}"
end
Shell
Rescued: #<RuntimeError: This exception will be rescued!>

Rescuing a class also rescues every subclass of it. RuntimeError sits under StandardError, which is why the rescue in this example catches it.

To respond to several exception classes in the same way, list them in one rescue clause, separated by commas:

Ruby
begin
  raise TypeError, "This exception will be rescued!"
rescue TypeError, KeyError => e
  puts "Rescued: #{e.inspect}"
end
Shell
Rescued: #<TypeError: This exception will be rescued!>

When different errors need different handling, chain multiple rescue blocks on the same begin block. Ruby runs the first clause whose class matches the raised exception:

Ruby
begin
  raise KeyError, "This exception will be rescued!"
rescue TypeError => e
  puts "Rescued: #{e.inspect}"
rescue KeyError => e
  puts "Rescued, but with a different block: #{e.inspect}"
end
Shell
Rescued, but with a different block: #<KeyError: This exception will be rescued!>

Method bodies are implicit begin blocks, so a rescue clause can sit directly in a method definition without the begin keyword:

Ruby
def parse_number(input)
  Integer(input)
rescue ArgumentError
  0
end
 
parse_number("42")   # => 42
parse_number("five") # => 0

The Exception Hierarchy

Ruby’s exception classes form a tree, which lets a single rescue clause catch a whole family of errors without listing each one. The built-in classes on Ruby 3.4, trimmed to the ones you’re most likely to meet (Ruby also defines subclasses for IO buffers, encodings, and Ractors):

Shell
- NoMemoryError
- ScriptError
    - LoadError
    - NotImplementedError
    - SyntaxError
- SecurityError
- SignalException
    - Interrupt
- StandardError (default for `rescue`)
    - ArgumentError
        - UncaughtThrowError
    - EncodingError
    - FiberError
    - IOError
        - EOFError
        - IO::TimeoutError
    - IndexError
        - KeyError
        - StopIteration
            - ClosedQueueError
            - Ractor::ClosedError
    - LocalJumpError
    - Math::DomainError
    - NameError
        - NoMethodError
    - NoMatchingPatternError
        - NoMatchingPatternKeyError
    - RangeError
        - FloatDomainError
    - RegexpError
        - Regexp::TimeoutError
    - RuntimeError (default for `raise`)
        - FrozenError
    - SystemCallError
        - Errno::*
    - ThreadError
    - TypeError
    - ZeroDivisionError
- SystemExit
- SystemStackError
- fatal (internal)

When a rescue clause names no class, StandardError is assumed. Everything under the StandardError branch — ArgumentError, NoMethodError, TypeError, and the rest — is caught by a bare rescue; everything outside it, like SignalException and SystemExit, is not. That split is deliberate, and it’s the subject of the next section.

The odd one out is fatal, which Ruby uses internally for unrecoverable situations like deadlocks. Its class constant is hidden, so you cannot name it in a rescue clause, and neither a bare rescue nor rescue StandardError catches it. A blanket rescue Exception can catch a deadlock-raised fatal — one more reason not to write one.

SystemCallError shows how useful the tree is in practice. Low-level system operations, like reading a file, fail in platform-dependent ways, so Ruby defines a separate Errno::* class for each operating system error. Reading a file that doesn’t exist raises one of them:

Ruby
File.read("does/not/exist")
Shell
$ ruby r.rb
r.rb:1:in 'IO.read': No such file or directory @ rb_sysopen - does/not/exist (Errno::ENOENT)
	from r.rb:1:in '<main>'

Every Errno::* class is a subclass of SystemCallError, so rescuing the parent covers missing files, permission errors, and the rest of the family in one clause — and the exception object still tells you which specific error occurred:

Ruby
begin
  File.read("does/not/exist")
rescue SystemCallError => e
  puts e.class # => Errno::ENOENT
  puts e.class.superclass # => SystemCallError
  puts e.class.superclass.superclass # => StandardError
end

Rescuing Too Broadly

Catching a class high up the tree means catching every subclass under it — including ones you didn’t anticipate. Here’s a program that reads a config file named by its first argument:

Ruby
# $ ruby example.rb config.yml
def config_file
  ARGV.firs # A typo: this should be ARGV.first.
end
 
begin
  File.read(config_file)
rescue
  puts "Couldn't read the config file"
end
Shell
Couldn't read the config file

The message blames the config file, but the file was never the problem: ARGV.firs is a typo. The bare rescue catches StandardError, and NoMethodError is a StandardError subclass, so the bug is reported as a missing file. Printing the exception object shows what went wrong:

Ruby
begin
  File.read(config_file)
rescue => e
  puts e.inspect
end
Shell
#<NoMethodError: undefined method 'firs' for an instance of Array>

Without any rescue at all, Ruby’s own report for the typo’d call — here in a file containing ARGV.firs on its own — pinpoints the problem and suggests the fix:

Shell
$ ruby r.rb
r.rb:1:in '<main>': undefined method 'firs' for an instance of Array (NoMethodError)

ARGV.firs
    ^^^^^
Did you mean?  first

The cure is to rescue the specific classes the operation can raise, and let everything else crash loudly:

Ruby
config_file = "config.yml"
 
begin
  File.read(config_file)
rescue Errno::ENOENT
  puts "File or directory #{config_file} doesn't exist."
rescue Errno::EACCES
  puts "Can't read from #{config_file}. No permission."
end

Now a missing file and a permission problem each get an accurate message, and a typo in the code fails with a backtrace that points at the typo.

Why You Should Not Rescue Exception

Exception is the root of the tree, so rescue Exception catches everything — which sounds like maximum safety and works out as the opposite. Two exceptions most programs must not swallow live outside the StandardError branch:

  • SignalException (and its subclass Interrupt) is raised when the outside world tells your process to stop: Ctrl-C in a terminal sends SIGINT, and process managers send SIGTERM on shutdown or deploy.
  • SystemExit is raised by exit itself — it’s how Ruby unwinds the stack when your own code decides to stop.

A plain rescue leaves both alone. This program sends itself the same signal Ctrl-C would:

Ruby
begin
  Process.kill("INT", Process.pid)
  sleep 1
rescue StandardError => e
  puts "This will not be printed."
end
Shell
$ ruby r.rb
r.rb:2:in 'Process.kill': Interrupt
	from r.rb:2:in '<main>'

The Interrupt sails past rescue StandardError and stops the process with exit status 130, exactly as it should. Swap in rescue Exception, and the signal is trapped:

Ruby
begin
  Process.kill("INT", Process.pid)
  sleep 1
rescue Exception => e
  puts "Rescued: #{e.class}"
end
Shell
Rescued: Interrupt

The program shrugs off the interrupt and keeps running. Wrap a long-running loop in a rescue like this and Ctrl-C stops working; a process manager trying to restart your app during a deploy gets ignored until it escalates to SIGKILL. rescue Exception traps exit the same way:

Ruby
begin
  exit
rescue Exception => e
  puts "Rescued: #{e.class}"
end
 
puts "Still running!"
Shell
Rescued: SystemExit
Still running!

It also swallows LoadError, SyntaxError, and NoMemoryError — failures you want to hear about immediately. If your code needs to survive Ctrl-C for a graceful shutdown, rescue Interrupt explicitly and make the handler stop the process when it’s done. Reserve rescue Exception for one pattern: cleanup code that reports and re-raises, covered in the Re-Raising Exceptions section.

Swallowing Exceptions

Even within the StandardError branch, a rescue that’s broader than the failure you expect can hide bugs. This example wants to handle an unreadable file:

Ruby
image = nil
 
begin
  File.read(image.filename)
rescue
  puts "File can't be read!"
end
Shell
File can't be read!

The image variable is nil, so calling #filename on it raises a NoMethodError — a StandardError subclass, and therefore quietly caught by the bare rescue. The output claims a file problem while the code has a nil problem, and nothing in the logs points at it.

undefined method 'filename' for nil (NoMethodError)

Shell
r.rb:2:in '<main>': undefined method 'filename' for nil (NoMethodError)

Cause: A method was called on nil — a variable that was never assigned, a hash lookup that found nothing, or a query that returned no record. Ruby 3.4 prints for nil; older Rubies printed for nil:NilClass, so search results quoting that form describe the same error.

Fix: Track down where the nil came from and handle that case before the call — the caret marker in the error output points at the exact call that failed. And to keep a rescue from hiding this class of bug in the first place, rescue the exception the operation can raise instead of rescuing everything:

Ruby
image = nil
 
begin
  File.read(image.filename)
rescue Errno::ENOENT
  puts "File can't be read!"
end
Shell
$ ruby r.rb
r.rb:4:in '<main>': undefined method 'filename' for nil (NoMethodError)

  File.read(image.filename)
                 ^^^^^^^^^

With the rescue narrowed to Errno::ENOENT, the missing-file case still gets its friendly message, and the nil bug crashes with a backtrace instead of hiding behind one.

Re-Raising Exceptions

Sometimes a broad rescue is legitimate: cleanup that must run no matter what killed the block, like removing temp files or marking a job as failed. The rule that keeps it safe is to re-raise — handle your part, then let the exception continue up the stack as if you’d never caught it. A bare raise inside a rescue clause re-raises the current exception unchanged:

Ruby
begin
  clean_up_temp_files
rescue Exception => e
  Appsignal.report_error(e)
  raise
end

(clean_up_temp_files stands in for whatever work needs the guard.) The block reports the exception, runs no risk of swallowing an Interrupt forever, and the process still stops the way Ruby intended — with the original class, message, and backtrace intact. AppSignal’s Ruby exception-handling docs cover tagging, namespaces, and reporting from custom integrations.

For the mechanics of re-raising in depth — including replacing an exception with a more meaningful one — see the reraising section of our ensure/retry guide.

When a broad rescue is unavoidable, report the exception before you re-raise it. With AppSignal’s Ruby error tracking, that’s one line — Appsignal.report_error(e) — so every rescued-and-re-raised exception still reaches your dashboard with its full backtrace, grouped by cause.

Ensure, Retry, and Cleanup

Two more keywords round out Ruby’s exception handling. An ensure clause runs whether the block succeeded, rescued, or is about to crash — the right place to close files and release connections. retry, called from inside a rescue clause, jumps back to the top of the begin block and runs it again, which is the foundation of retry logic for flaky network calls (always paired with a retry limit, unless you enjoy infinite loops).

retry is only valid inside a rescue clause; anywhere else, the parser rejects the file before it runs, with Invalid retry without rescueEnsure, Retry and Reraise Exceptions in Ruby covers that SyntaxError, along with backoff patterns and a worked example of a resilient API client.

Custom Exception Classes

Rescuing specific classes works best when your own code raises specific classes. Defining an error class per failure mode — InvalidConfigError, PaymentDeclinedError — lets callers rescue your library’s failures as a group without catching unrelated bugs. Inherit from StandardError, never from Exception, so a plain rescue in calling code behaves the way this whole guide assumes.

Diving into Custom Exceptions in Ruby covers the full topic: naming conventions, default messages, custom data on exception objects, and the errors that come from defining them wrong. For a framework-level example of the same ideas, see Handling Exceptions in Grape.

What to Rescue When You’re Unsure

Documentation doesn’t always list what an operation can raise, so build the rescue clause from evidence. Start with a rescue for StandardError that prints the exception object, and run the code in the scenarios you care about:

Ruby
begin
  File.open("/tmp/appsignal.log", "a") { |f| f.write("Starting AppSignal") }
rescue => e
  puts e.inspect
end
Shell
#<Errno::EACCES: Permission denied @ rb_sysopen - /tmp/appsignal.log>

Each exception you see this way earns its own rescue clause with a message that helps the person hitting it:

Ruby
file = "/tmp/appsignal.log"
 
begin
  File.open(file, "a") { |f| f.write("AppSignal started!") }
rescue Errno::ENOENT
  puts "File or directory #{file} doesn't exist."
rescue Errno::EACCES
  puts "Cannot write to #{file}. No permissions."
end

When several of the observed classes share a parent — as Errno::ENOENT and Errno::EACCES share SystemCallError — rescuing the parent is a reasonable middle ground. The direction of travel matters more than the destination: grow the rescue list from errors you’ve seen, rather than starting broad and hoping.

Wrapping Up

Raise with a class and a message, rescue the most specific class that fits, and let everything you didn’t expect crash with a backtrace. A bare rescue means StandardError — which is almost always what you want — and rescue Exception belongs only in report-and-re-raise cleanup blocks. When an exception does crash your app, the follow-up guides pick up from here: Reading and Understanding Ruby Stack Traces for decoding the output, Debugging Exceptions in Rails for the framework side, and Exceptional Error Reporting for tracking exceptions in production.

Have any questions about raising or rescuing exceptions in Ruby? Let us know at @AppSignal — we’d also love to hear how you liked this article, or what you’d like us to cover next.

P.S. If you’d like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!

Frequently asked questions

Should I rescue Exception or StandardError in Ruby?
Rescue StandardError, which is what a bare rescue already does. Rescuing Exception also traps Interrupt, SystemExit, and other signals your process needs, so it cannot be stopped cleanly. Reserve Exception for cleanup code that re-raises immediately.
What does a bare rescue catch in Ruby?
A rescue with no class catches StandardError and every subclass of it, including NoMethodError, ArgumentError, and TypeError. It does not catch exceptions outside the StandardError branch, such as SignalException, SystemExit, LoadError, SyntaxError, or NoMemoryError.
How do I rescue multiple exceptions in one rescue block in Ruby?
List the classes after the rescue keyword, separated by commas, followed by an optional variable, as in rescue TypeError, KeyError => e. To handle different errors differently, chain several rescue blocks on the same begin block, one per class.
What happens if you raise something that is not an exception in Ruby?
Ruby raises a TypeError with the message exception class/object expected. The raise keyword accepts an exception class, an exception instance, or a string, which becomes the message of a RuntimeError. Anything else fails with that TypeError.
Why does my Ruby rescue block not catch Ctrl-C?
Ctrl-C sends SIGINT, which Ruby raises as Interrupt, a subclass of SignalException — outside the StandardError branch a plain rescue catches. That is deliberate: trapping it would make the process impossible to stop. Rescue Interrupt explicitly for a graceful shutdown.

Published , Updated

Wondering what you can do next?

  • Share this article on social media

Become our next author!

Find out more
$appsignal install

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal