Exceptions in Ruby

Why do we do exception handling? Sometimes things don’t execute the way they are designed to, for reasons outside of our code’s responsibilty. Maybe your code relies on something external, for example, scraping, an external server, an API. If your code uses external input, exceptions are a way of handling any exceptions to the expected outcome.

In many ways, exceptions are similar to conditionals. The exception handling block in Ruby, the begin...rescue block, is not unlike an if...else conditional statement; they both control flow. Exceptions account for the possibility of something happening and accounting for that, not unlike conditional statements.

Let’s look at a simple example method in Ruby:

1
2
3
def plus_one(num)
  num += 1
end

When we run plus_one("22"), as you can guess, we’re going to get the following error returned:

1
# => TypeError: String can't be coerced into Fixnum

If we wanted to avoid that TypeError from happening, which would break our program at runtime, we could write a conditional that expects input that’s not a number and sanitizes it:

1
2
3
4
5
6
7
def plus_one(num)
  if !num.is_a?(Fixnum)
    num.to_f
  else
    num += 1
  end
end

But what if you wanted to know about that error, not just go ahead and fix it? Or maybe you really cannot fix the input yourself, because it’s coming from somewhere else. Or, most commonly, you just need to handle the error and keep things running.

That’s where exception handling comes in:

1
2
3
4
5
6
7
def plus_one(num)
  begin
    num += 1
  rescue
    puts "Can't add a #{num.class} '#{num}' to an integer. Please make sure the input is an integer."
  end
end

The begin...rescue block works as such that when begin is triggered, it’s going to try to execute whatever code is in that block, and, if for whatever reason it can’t (for example, a TypeError), it’s going to fall down to the rescue block and execute the code in that block instead. This way, our program won’t crash if there’s an error.

Our contrived example doesn’t really lend itself to anything other than what it is, but the fact that exception handling prevents our program from crashing is very important. If our code relies on data that is external, we don’t have full control over it. It can change. That’s the power of exception handling: our program will not break if what it relies on changes.

So our begin...rescue block is pretty cool, but we can take exception handling even further if we need to.

In our above begin...rescue block, num += 1 will only be called if an error isn’t raised during its execution. What if, regardless of that code being run, we want something else to always execute? That’s where the ensure block comes in.

1
2
3
4
5
6
7
8
9
def plus_one(num)
  begin
    num += 1
  rescue
    puts "Can't add a #{num.class} '#{num}' to an integer. Please make sure the input is an integer."
  ensure
    puts "You'll always see me!"
  end
end

Ensure happens after both the begin and rescue blocks are ran, and will always execute, hence the name ensure.

Exceptions like begin...rescue are great for just that, throwing exception messages. While they act like conditional expressions, they really should never be used for managing conditional flow. They should be reserved for providing information when something went wrong, handling what happens when that error occurs, and keep things flowing. The reasoning for this isn’t a stylistic choice: figuring out where and why something went wrong takes time. Exception handling is slow, and conditional flow, part of everyday development, should be fast.

Up next, I’ll talk about another option for programmatic flow in Ruby: try...catch.

Comments