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.
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.
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 => e # ... end
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
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
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 directive is an exception handler that rescues exceptions raised in controller actions. The rescue_from 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
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
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