Ruby to the Rescue
Published: 2023-01-30 21:42:09

## 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:


```js
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`.

```ruby
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

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

Ruby

```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.

```ruby
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.

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

```ruby
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`:

* [https://avdi.codes/throw-catch-raise-rescue-im-so-confused/](https://avdi.codes/throw-catch-raise-rescue-im-so-confused/) 
* [http://rubymonk.com/learning/books/4-ruby-primer-ascent/chapters/41-exceptions/lessons/93-throw-and-catch](http://rubymonk.com/learning/books/4-ruby-primer-ascent/chapters/41-exceptions/lessons/93-throw-and-catch)