Exception Handling in Ruby

What is exception handling?

Software systems can be quite prone to error conditions. Systems that involve user interaction are more vulnerable to exceptions as they attract errors at multiple fronts. Errors can take many forms - syntactical errors, network errors, form input errors, invalid authentication errors etc. If not accounted for, these can affect user experience and can even manifest as security loopholes, enabling attackers to compromise the system.

Exception handling is used to prevent system failures and to save the user from lousy error messages.

In most cases, when an error condition occurs, the programming language forces the program to terminate and therefore program control is lost. Exception handling allows you (or the program) to not lose control and to be able to deal with what happens after the error has occurred. This gives you some room to make amends - to show affable error messages, to release some resources or to conveniently redirect the user to a relevant page. 

Exception handling helps you clear up the mess, without making it look like one.

It is the process of accommodating error conditions and responding to them programmatically, resulting in execution of a presumably alternate, but already planned sequence of code.

Exception handling in Ruby

In the vast majority of languages, exception handling is performed using “try, throw and catch” mechanisms. When the try block is encountered, the compiler becomes super aware in the lookout for an exception (thrown either manually, or by the language itself) and instructs the catch block to deal with it. Ruby, however decided to go with different names for their conventional exception handling system. 

I find the Ruby terminology for dealing with exceptions a lot more poetic. 

There is a beginning, followed by errors raised and eventually rescued. And like all good things, it comes to an end, unless you want to retry.

begin
    # raise exception here

rescue
    # raised exception handled here

retry
    # retrying the code in the begin block


ensure
    # Always executed

end

Exception handling in Ruby primarily consists of -

Begin - end block, raise, rescue and ensure are analogous to the programming concepts of try, throw, catch and finally. Let’s try to understand them.

Note: Ruby also provides throw and catch functions for handling exceptions, which are lightweight counterparts of raise and rescue that do not build stack traces of the exceptions. Even though raise and rescue based mechanisms are more prevalent among Ruby developers, we’ll see how Ruby’s throw and catch mechanisms can help later in this post.

2.1 Begin - end block

The error prone part of your code comes in the begin block. The corresponding exception handling, through the raise and rescue mechanisms, also happens inside the begin block.

You can use begin blocks to sectionalize the different types of errors that are likely to show up. Differently erroneous codes can be put into different begin - end blocks and can accordingly be handled.

begin
    # code likely to give error comes in this block
    # raising and rescuing something
end

begin
    # raising and rescuing something else
end

Note: The body of a Ruby method can also act as begin-end block and thus does not require an explicit ‘begin’ call.

def raise_and_rescue
  # begin call not necessary 
  # raising and rescuing
end  

2.2 Raise

An exception can either be raised manually by the developer, using the raise command, or automatically by the language itself. Exceptions are raised automatically on incurring a syntactical error, like a variable not declared, a function not defined or an invalid math operation etc.

Based on your application, custom exception conditions can be catered to by manually raising errors. 

A very common use case would be a signup webpage that doesn’t submit the form until you have entered (and/or re-entered) a valid password. This sort of client-side proofing can save you from an unnecessary HTTP request to your server which might either affect database integrity or respond with an error message, which will need to be handled later. 

Let’s see how we can raise an exception in Ruby - 

begin  
    # if some condition is met or not met ->   
    raise "I am the error message!"
    # below lines are not executed    
end  

Here we are just raising an exception, not handling it (yet). This results in the program being terminated where the exception is raised, and as a result, the lines below are not executed. A language-generated error message is output as shown below - 

Ruby1.png

To prevent this, we can handle the exceptions in our own way, by using rescue blocks.

2.3 Rescue to the rescue

Fire.gif

Rescue blocks are like catch blocks that take control of the program after an exception has been raised. Raised exceptions can be followed by one or more rescue blocks to handle the exceptions based on their type. A generic rescue call looks like this - 

begin
    # if some condition is met or not met ->
    raise 'I am the error message!'

    puts 'I am not executed.'
rescue    
    puts 'Rescue to the rescue!'
end    

puts 'Amicably exiting ..'  

Once the exception is handled, execution resumes from after the begin block.

Ruby2.png

The rescue call defaults to a StandardError parameter. Rescue calls can however be made to handle specific exceptions. Some common Ruby errors (also common across other languages) are - 

Rescue blocks can be made to cater to certain types (classes) of exceptions.

In this way, raised exceptions can be followed by multiple rescue clauses, each responsible for handling a different type of exception. Let’s see how we can do this in code - 

begin  
  # error raised here 
rescue IndexError  
  # dealing in one way
rescue RangeError  
  # dealing in another way  
end 

Arguments can be used with the rescue clause to act as placeholders for the rescued exception - 

begin  
    raise 'I am the error message!'  
rescue StandardError => e  
    # inspecting the raised exception
    puts e.message

    # information about where the exception was raised  
    puts e.backtrace.inspect  
end 

Ruby3.png

The output tells how the exception was raised in line 2 of our code.

Specific types (classes) of exceptions can be raised even with custom error messages. For example -

begin
raise ZeroDivisionError, "Custom error message"
rescue ZeroDivisionError => e  
    # inspecting the raised exception
    puts e.message  
    puts e.backtrace.inspect
end 

Ruby4.png2.4 Else

Sometimes it’s possible that there was nothing to rescue from, that no exception was raised. The else clause can be used in this case as such - 

begin  
rescue StandardError => e  
    # inspecting the raised exception
    puts e.message  
    puts e.backtrace.inspect
else
    puts 'No exception to capture?'
end 

Ruby5.pngMagic variables

When exceptions are raised, Ruby assigns two variables - $! and $@ with information about the raised exception.

begin
raise ZeroDivisionError, "Custom error message"
rescue ZeroDivisionError => e
    # inspecting the raised exception
    puts $! # equivalent to e.message
    puts $@ # equivalent to e.backtrace.inspect
end 

Ruby6.png
2.5 Retry

Well, what if you don’t want to give up? You might want to keep trying till it works. 

undefined

This could be a common case when network connections are inconsistent. 

As I type this post on a Google document, at my home, on a poor internet connection, I can see the webpage continuously trying to keep me connected to the internet.

Till now we have seen how rescue blocks can be used to deal with what happens after an exception has occurred. The retry clause allows us to run the begin block again. This can come in handy after we have fixed what caused the exception to happen in the first place. 

Let’s see how retry can help, with a rather simple example. 

We know division of a number by zero is undefined. In the example below, we try to divide a by b (a / b). Initially the denominator, b is zero and therefore the division would raise a ZeroDivisionError. In the rescue block, we change the denominator to 1 and retry the whole thing.

a = 10
b = 0
begin
puts a/b
rescue ZeroDivisionError => e  
    # inspecting the raised exception
    puts e.message  
    puts e.backtrace.inspect
    b = 1
    retry
else
    puts 'No exception to capture?'
    puts 'Division was successful!'
end 

Ruby7.png

2.6 Ensure

Ensure is like the finally block that we have in other programming languages. It is there to always run at the end of each begin block. Regardless of any control sequence that the program ends up following, the ensure block is always executed at the end of it’s parent begin block. 

begin
    # if some condition is met or not met ->
    raise 'I am the error message!'
rescue    
    puts 'Rescue to the rescue!'
ensure
    puts 'Always executed at the end.'
end    

Ruby8.pngEnsure blocks are executed even if there isn’t a rescue block to handle a thrown exception. 

begin
    # if some condition is met or not met ->
    puts 'Begin!'
    raise 'I am the error message!'
ensure
    puts 'Executed even when there is no rescue clause'
end 

Ruby9.png

Note how in this case, even though the ensure block is executed at the end, the raised exception manifests as a runtime error after the ensure block.

Ensure blocks are generally used to free up any resources after an exception has been raised eg. closing database connections, closing file handlers etc. Without such blocks, the program abruptly terminates without clearing the resources.

Throw and catch in Ruby - lightweight alternatives

It’s not a good idea to exceptionally over-handle your code. Extensive raising and rescuing can take a toll on the system’s performance. 

Each time an exception is raised, a stack trace is built that helps in debugging the error. Too many of such handled exceptions can soon become a terrible bottleneck.

Turns out there is a lightweight alternative to the conventional approach that allows you to smoothly transfer program control out of complex nested blocks. Throw clauses and catch blocks in Ruby are linked through a common label name that both share.

throw :labelname
# ..
catch :labelname do
# ..
end

You can also use conditional throw calls as such - 

throw :labelname if a > b
# ..
catch :labelname do
# ..
end

Throw and catch in Ruby are quite different from conventional throw-catch mechanisms in other languages. In Ruby, they are used more for terminating execution quickly, and not as much for handling exceptions. 

It can come in handy when you are working with nested for loops and you want to get out of the computationally expensive mess when a condition is met, without having to use multiple break statements. Throw helps program control to move around swiftly to help programs execute faster, in a more efficient manner, compared to slow conventional begin/raise/rescue mechanisms that are more concerned about the raised exception, where it happened etc.

The way that throw and catch are used is that throw calls are made from inside catch blocks, transferring control from deep inside a nested construct, back to the level of the catch block in the code.

catch(:get_me_out) do
  # arbitrary nested for loop
  for a in 1..5 do
    for b in 1..5 do
      # arbitrary condition
      if a + b == 7
        # throwing to get out of the nested loops
        throw(:get_me_out)
      end
    end
  end
end

puts 'Good to go!'

Ruby10.png

A throw function can also be called with a custom message as an argument, which the catch block ends up returning after something is thrown.

thrown_msg = catch(:get_me_out) do

  # arbitrary nested for loop
  for a in 1..5 do
    for b in 1..5 do

      # arbitrary condition
      if a + b == 7

        # throwing to get out of the nested loops
        throw(:get_me_out, 'Inside this mess, get me out')
      end
    end
  end
end

puts thrown_msg
puts 'Good to go!'

Ruby11.png

Custom Exception class in Ruby

We can create our own Exception classes that cater to specific requirements in our projects. This can be done by inheriting from the provided StandardError class.

class MyCustomError < StandardError
end

raise MyCustomError

Ruby12.png

It’s good to have an error message to accompany the raised exception. We can do this by overriding the initialize method for our custom class with a default msg argument.

class MyCustomError < StandardError
  def initialize(msg="Custom error message")
    # to override the method
    super
  end
end

raise MyCustomError

Ruby13.png

Wrapping it up

wrap.gif

In this post, we looked at -

All in all, we learnt how Ruby prefers a different terminology for dealing with exceptions. Except for the retry function, the begin / raise / rescue paradigm resonates with the conventional try / throw / catch setup used across most other programming languages. Throw and catch in Ruby however help in quickly getting yourself out of complex constructs. We also saw how we can create our application-specific custom Exception classes that inherit from the StandardError class.

Try ScoutAPM

Thanks for reading our blog. Now that you have a handle on handling exceptions, you need the best way to find them in your application.  Sign up here today for a free trial or contact us with any queries you might have  Want to show off Scout to your fellow developers? Click here for some free ScoutAPM swag.