javascriptjavaphprubydotnet

Ruby handling exceptions

Exception Handling in Ruby

In Ruby, all exceptions and errors are extensions of the Exception class. While this may seem intuitive, exception handling in Ruby is a touch more nuanced than you might expect thanks to the designed hierarchy of Ruby exceptions.

The begin-rescue

Similar to PHP's try-catch, Ruby's exception handling begins with the begin-rescue block. In a nutshell, the begin-rescue is a code block that can be used to deal with raised exceptions without interrupting program execution. In other words, you can begin to execute a block of code, and rescue any exceptions that are raised.

Rescuing Exceptions

By default, begin-rescue rescues every instance of the StandardError class. This includes no method errors, type errors, runtime errors, and every custom error that is intended to be rescued within a Ruby application (see Raising Exceptions in Ruby for more information). To rescue every StandardError, simply wrap the designated section of code in a begin-rescue block:

begin
  # ...
rescue => e
  # ...
end

When a StandardError exception is raised within the begin block, an instance of it will be passed to the rescue block as the variable e (for more information about the structure of Ruby's Exception class, see Raising Exceptions in Ruby).

Rescuing Specific Exceptions

While rescuing every exception raised is great for simplistic implementations—such as generalizing API error responses—best practice is to rescue for specific exceptions. To do this, let's rewrite the generic begin-rescue block above to specifically rescue StandardError exceptions:

begin
  # ...
rescue StandardError => e
  # ...
end

Although the difference may be subtle, by following the rescue command with a class name, then only exceptions of the defined type will be rescued. For example, if we wanted to rescue all argument errors, we could structure our begin-rescue block like this:

begin
  # ...
rescue ArgumentError => e
  # ...
end

But, what if we want to rescue more than one exception type? Much like an if-elsif-else chain, a begin-rescue block can have multiple rescues—which, when combined with a check for the StandardError class, allows you to logically adapt to any and all issues that may arise:

begin
  # ...
rescue ArgumentError => e
  # ...
rescue TypeError => e
  # ...
rescue => e
  # ...
end

Rescuing All Exceptions

While it may be tempting to rescue every child of the Exception class, it is generally considered bad practice due to the way the Ruby exception hierarchy is structured. The reason for this is that, while all Ruby exceptions and errors are an extension of the Exception class, many of them are reserved for use internally by Ruby. For example, SignalException::Interrupt is used to signal that Ctrl-C has been detected during the execution of a script.

If you were to rescue every child of the Exception class, then the Interrupt exception wouldn't work as expected. That said, if you do want to rescue every exception that is raised in Ruby, the same begin-rescue block can be used to specifically rescue all Exception exceptions:

begin
  # ...
rescue Exception => e
  # ...
end

Using the above begin-rescue block will rescue every exception and error that is raised, from interrupts to syntax errors, and even memory errors, so use it sparingly and with caution.

Exception Handling in Ruby on Rails

Generally speaking, the begin-rescue block works as intended in Ruby on Rails. That said, in order to better handle the specific use cases that can come up in the Rails architecture, additional methods have been made available for use within a Ruby on Rails application.

The rescue_from

The rescuefrom directive is an exception handler that rescues exceptions raised in controller actions. The rescuefrom directive rescues the specified exceptions raised within a controller, and reacts to those exceptions with a defined method. For example, the following controller rescues User::NotAuthorized exceptions and passes them to the deny_access() method:

class ApplicationController < ActionController::Base
  rescue_from User::NotAuthorized, with: :deny_access

  protected
    def deny_access(exception)
    # ...
    end
end

The advantage to rescue_from is that it abstracts the exception handling away from individual controller actions, and instead makes exception handling a requirement of the controller. This not only makes exception handling within controllers more readable, but also more regimented.

Exception Handling in Sinatra

While the additional exception handling within Ruby on Rails is focused on controller exceptions, Sinatra offers a few additional ways to deal with raised exceptions.

Logging Exceptions with dump_errors

Enabled by default, dump_errors is a Sinatra setting that allows exception backtraces to be written directly to STDERR. In the context of a development server, this information can be incredibly valuable, but might be more difficult to act upon in a production environment. When used in conjunction with traditional log aggregation and analysis techniques, however, this is a great way to collect exception data as it happens without needing to reproduce it.

Propagating Exceptions with raise_errors

By default, exceptions raised within Sinatra do not leave the application. What this means is that, when an exception is raised, it is rescued and mapped to internal error handlers. By enabling the raise_errors setting, these exceptions are raised outside of the application, allowing the server handler or Rack middleware to deal with exceptions.

Enable Classy Error Pages with show_exceptions

When working in a development environment, being able to quickly react to exceptions is crucial, which is why exception backtraces and environment information are on-screen by default in these environments. While this setting is turned off in production environments, it can be enabled or disabled by updating the show_exceptions setting.

Custom Error Handling

While Sinatra has built-in support for graceful error handling, it is sometimes desirable to write custom logic to handle raised errors. To do this, an error block can be used in the same way as a rescue block. For example, to rescue a User::NotAuthorized error, the following directive would work in Sinatra:

error User::NotAuthorized do
  # ...
end

Similarly, if we wanted to rescue all exceptions that are raised in a Sinatra application, we could use the following error handler:

error do
  # ...
end