Ruby to the Rescue for Error Handling

Engineering Insights

Taylor Kearns
#
Min Read
Published On
March 13, 2025
Updated On
April 25, 2025
Ruby to the Rescue for Error Handling

TL;DR

Though we frequently use the terms "throw" and "catch" when discussing error handling, we must be careful in Ruby to use the more accurate terms "raise" and "rescue". "Throw" and "catch" have a significantly different meaning in Ruby.

The Problem

The other day we were writing an RSpec test and were surprised to find that we couldn't get this test to pass:

expect { throw StandardError.new("error") }.to raise_error

But we could get this test to pass:

expect { raise StandardError.new("error") }.to raise_error

Aren't these basically the same thing, with a minor syntactic difference? Raise an error, throw an error, same idea right? In Javascript we use try..catch for handling errors. Isn't it the same idea with Ruby? Not really. Ruby has two similar looking statements: try..catch and raise..rescue. Even though we can technically use each of these statements in similar situations and get ostensibly similar results, the intent behind the two structures is quite different.

In Ruby, Don't Catch Errors; Rescue Them

The main source of confusion (for me, anyway) is in the term catch. When I think catch, I think error. In Javascript if we want to handle an error we write something like:

try {
  myFunction()
catch (error) {
  console.error(`There was an error: ${error}`)
}

So in Ruby wouldn't I do something similar? Yes, but not with try..catch. In Ruby we use raise..rescue.

def my_function
  do_the_work
rescue => error
  logger.error("There was an error: #{error}")
end

Ruby's raise..rescue is the corollary to Javascript's try..catch. If we compare the complete syntax of both structures, especially when we employ the optional begin keyword, the patterns look even more similar:

Javascript

try {
  // do stuff
catch {
  // catch the error
finally {
  // do this no matter what
}

Ruby

begin
  # do stuff
rescue => e
  # catch the error
ensure
  # do this no matter what
end

So what is catch for in Ruby, if not for errors?

The throw..catch statement in Ruby is actually intended to be used as a part of an expected workflow rather than for an exceptional (i.e. error) scenario. throw..catch can be used as a means of escaping from a flow once a particular requirement is met. In the example below, we have an array of birds. As soon as we find a goose, we want to stop execution of the each loop. This example is contrived, but we can imagine that if we were performing expensive procedures in each iteration, we would want to bail out as soon as we are able.

birds = ["duck", "duck", "goose", "duck"]

catch(:goose) do
  birds.each do |bird|
    puts bird

    if bird == "goose"
      throw(:goose)
    end
  end
end

$> duck
$> duck
$> goose

catch is meant to be used with throw, and likewise throw is meant to be used with catch. Though we may see code in the wild in which an error is thrown and later rescued, that's not really what it's intended for. In fact if you try that out, take a look at the error message.

def throw_rescue
  throw StandardError, "There was an error."
rescue => e
  puts e
end
throw_rescue
$> uncaught throw StandardError

The uncaught throw tells us explicitly that we should be using catch somewhere. Additionally, look what we're missing: a stacktrace. Because throwing and catching are meant to be part of an expected flow, it makes sense that they wouldn't have a a stacktrace. So in this case we get a misleading exception (uncaught throw vs. StandardError) and no additional information about the source of the error.

What makes this even more confusing is that we can throw more than just symbols. As we see above, we can throw class names, like Exceptions. But we really shouldn't, as the example above should demonstrate.

Looking back at our original question about our RSpec test, we can see now why this test won't ever pass:

expect { throw StandardError.new("error") }.to raise_error

We can't expect throwing an error to raise that error, because throwing and raising aren't the same thing in Ruby. If you want to handle errors, do so with raise..rescue, not try..catch.

Here are some additional resources on raise..rescue and throw..catch:

Learn more about how The Gnar builds Ruby on Rails applications. 

Taylor Kearns

Related Insights

See All Articles
Engineering Insights
Why Your AI Coding Agent Keeps Making Bad Decisions (And How to Fix It)

Why Your AI Coding Agent Keeps Making Bad Decisions (And How to Fix It)

AI coding agents making bad decisions? The frustration comes from two fixable problems: assumptions and code quality. Here's how to get consistently good results.
Product Insights
From Dashboards to Decisions: Why Traditional BI Can't Keep Up

From Dashboards to Decisions: Why Traditional BI Can't Keep Up

Stop waiting days for dashboards. Learn how BI2AI uses LLMs and RAG to eliminate the analyst bottleneck and turn complex data into instant executive decisions.
Product Insights
Are Your Legacy Systems Bleeding You Money?

Are Your Legacy Systems Bleeding You Money?

Technical debt now accounts for 40% of IT balance sheets, with companies paying a 10-20% surcharge on every new initiative just to work around existing problems. Meanwhile, organizations with high technical debt deliver new features 25-50% slower than competitors. Features on your six-month roadmap? They're shipping them in three weeks.
Previous
Next
See All Articles