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.
How to check Ruby syntax to identify exceptions
rescueclauses are used to tell Ruby which exception or types of exceptions we want to handle. The syntax for the
begin # may raise an exception rescue AnException # exception handler rescue AnotherException # exception handler else # other exceptions ensure # always executed end
The code between
rescueis where a Ruby exception can occur. If an exception is encountered, the code inside the
rescueclause gets executed. For each
rescueclause, the raised Ruby exception is compared against each parameter and the match succeeds if the exception in the clause is the same as or a superclass of the thrown exception.
If the thrown Ruby exception does not match any of the specified exception types, the
else block gets executed. The
ensureblock is always executed whether a Ruby exception occurs or not.
As an example:
#!/usr/bin/ruby begin file = open("/tmp/myfile") rescue Errno::ENOENT p "File not found" else p "File opened" end
In the above example, a file is attempted to be opened in the
rescueblock catches a “File not found” Ruby exception in case the file is not found at the location. If the file is found, the
elseblock gets executed.
Running the above code produces the following result if the file is not found:
"File not found"
If the file is found, the following output is produced:
"File not found"
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