Rescuing Exceptions in Ruby: A Primer

Tom de Bruijn

Tom de Bruijn on

Rescuing Exceptions in Ruby:
A Primer

At AppSignal we provide error tracking for Ruby applications. To do so, we capture all exceptions applications throw at us and notify developers as they happen.

It can be difficult to get exception handling right. In this article we'll explain how it works, what problems bad handling can cause and how to rescue exceptions properly.

Rescuing exceptions

By rescuing exceptions in Ruby you can prevent your application from crashing the moment something goes wrong. With a begin .. rescue block you can specify an alternative path for your application when an error occurs.

begin "config.yml" rescue puts "No config file found. Using defaults." end

It's also possible to specify which exceptions should be rescued. When specifying an exception class, all subclasses of this exception will also be captured.

begin "config.yml" rescue SystemCallError => e puts e.class # => Errno::ENOENT puts e.class.superclass # => SystemCallError puts e.class.superclass.superclass # => StandardError end

In the example above you can see the exception Errno::ENOENT is caught when its parent SystemCallError is being rescued.

Rescuing too high up in the exception chain

It's important not to rescue exceptions too high up the Exception chain. When you do, all subclassed exceptions will also be caught, making the rescue block's capture too generic.

Here's a program that reads a config file based on the argument passed to the program.

# $ ruby example.rb config.yml def config_file ARGV.firs # Note the typo here, we meant `ARGV.first`. end begin config_file rescue puts "Couldn't read the config file" end

The error message says it couldn't read the config file, but the real problem was a typo in the code.

begin config_file rescue => e puts e.inspect end #<NoMethodError: undefined method `firs' for []:Array>

The default exception class caught by a begin .. rescue block is StandardError. If we don't pass in a specific class, Ruby will rescue StandardError and all subclassed errors. NoMethodError is one of these errors.

Rescuing a specific exception class will help prevent unrelated errors from accidentally prompting a failure state. It also allows for more specific custom error messages that are more helpful for the end user.

config_file = "config.yml" begin config_file rescue Errno::ENOENT => e puts "File or directory #{config_file} doesn't exist." rescue Errno::EACCES => e puts "Can't read from #{config_file}. No permission." end

Rescuing Exception

It might still be tempting to rescue high up in the exception chain. Rescuing all errors an application can raise will prevent it from crashing. (100% uptime here we come!) However, it can cause a lot of problems.

The Exception class is the main exception class in Ruby. All other exceptions are subclasses of this class; if Exception is rescued all errors will be caught.

Two exceptions that most applications won't want to rescue are are SignalException and SystemExit.

SignalException is used when an outside source is telling the application to stop. This can be the Operating System when it wants to shut down, or a system administrator that wants to stop the application. Example

SystemExit is used when exit is being called from the Ruby application. When this is raised the developer wants the application to stop. Example

If we rescue Exception and these exceptions are raised while an application is currently running the begin ... rescue ... end block it cannot exit.

It's generally a bad idea to rescue Exception in normal situations. When rescuing Exception, you'll prevent SignalException and SystemExit to function, but also LoadError, SyntaxError and NoMemoryError, to name a few. It's better to rescue more specific exceptions instead.

Failures in tests

When Exception is rescued, using rescue Exception => e, other things beside your application could break. The test suite could actually be hiding some errors.

In minitest and RSpec assertions that fail will raise an exception to inform you about the failed assertion, failing the test. When they do, they raise their own custom exceptions, subclassed from Exception.

If Exception is rescued in a test or in the application code, it could be silencing an assertion failure.

# RSpec example def foo(bar) bar.baz rescue Exception => e puts "This test should actually fail" # Failure/Error: bar.baz # <Double (anonymous)> received unexpected message :baz with (no args) end describe "#foo" do it "hides an 'unexpected message' exception" do bar = double(to_s: "") foo(bar) end end

Expecting exceptions

Some code is meant to raise exceptions. In a test suite it's possible to simply silence the exception in order to have the test not fail when they are raised.

def foo raise RuntimeError, "something went wrong" end foo rescue RuntimeError

However, this doesn't test if an exception was raised or not. When the exception is not raised, your test won't be able to tell if the behavior is still correct.

It's possible to assert if the exception is raised, and if not, which exception was.

# expecting_exceptions_spec.rb # RSpec example def foo raise NotImplementedError, "foo method not implemented" end describe "#foo" do it "raises a RuntimeError" do expect { foo }.to raise_error(RuntimeError) end end
1) #foo raises a RuntimeError Failure/Error: expect { foo }.to raise_error(RuntimeError) expected RuntimeError, got #<NotImplementedError: foo method not implemented> with backtrace: # ./expecting_exceptions_spec.rb:4:in `foo' # ./expecting_exceptions_spec.rb:9:in `block (3 levels) in <top (required)>' # ./expecting_exceptions_spec.rb:9:in `block (2 levels) in <top (required)>' # ./expecting_exceptions_spec.rb:9:in `block (2 levels) in <top (required)>'

Re-raise Exception

An application should only capture exceptions as high up in the chain as the Exception class when there's a very good reason. For example, when there's some cleanup involved before exiting a block of code, like removing temporary files that really need to be removed.

One recommendation for when you absolutely have to rescue Exception, re-raise it after you're done handling the error. This way the Ruby exception handling can decide the fate of the process afterward.

ruby"/tmp/my_app.status", "w") { |f| "running" } begin foo rescue Exception => e Appsignal.add_error e"/tmp/my_app.status", "w") { |f| "stopped" } raise e end

Unsure what to rescue?

As mentioned earlier, it's good to be specific in what errors to rescue.

When you're unsure what exceptions an operation can raise, rescuing StandardError can be a good place to start. Run your code in different scenarios and see what exceptions it raises.

begin'/tmp/appsignal.log', 'a') { |f| f.write "Starting AppSignal" } rescue => e puts e.inspect end #<Errno::EACCES: Permission denied @ rb_sysopen - /tmp/appsignal.log>

Every time you come across a new exception, add specific rescue cases for those exceptions or its relevant parent class. It's better to be specific in what to rescue than to rescue too many exceptions.

begin file = '/tmp/appsignal.log', 'a') { |f| f.write("AppSignal started!") } rescue Errno::ENOENT => e puts "File or directory #{file} doesn't exist." rescue Errno::EACCES => e puts "Cannot write to #{file}. No permissions." end # Or, using the parent error class begin file = '/tmp/appsignal.log', 'a') rescue SystemCallError => e puts "Error while writing to file #{file}." puts e end

This concludes our primer on exceptions handling in Ruby. Let us know at @AppSignal if you want to know more, or have a specific question. If you want to get a better insight in where and how often exceptions are raised in your app, give AppSignal a try.

Tom de Bruijn

Tom de Bruijn

Tom is a developer at AppSignal, organizer, and writer from Amsterdam, The Netherlands.

All articles by Tom de Bruijn

Become our next author!

Find out more

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps