Visit AppSignal.com

Ruby Magic

Rescuing Exceptions in Ruby:
A Primer

Tom de Bruijn on

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.

1
2
3
4
5
begin
  File.read "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.

1
2
3
4
5
6
7
begin
  File.read "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.

1
2
3
4
5
6
7
8
9
10
# $ ruby example.rb config.yml
def config_file
  ARGV.firs # Note the typo here, we meant `ARGV.first`.
end

begin
  File.read 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.

1
2
3
4
5
6
begin
  File.read 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.

1
2
3
4
5
6
7
8
config_file = "config.yml"
begin
  File.read 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 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.

1
2
3
4
5
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.

1
2
3
4
5
6
7
8
9
10
11
# 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
2
3
4
5
6
7
8
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.

1
2
3
4
5
6
7
8
9
File.open("/tmp/my_app.status", "w") { |f| "running" }

begin
  foo
rescue Exception => e
  Appsignal.add_error e
  File.open("/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.

1
2
3
4
5
6
begin
  File.open('/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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
begin
  file = '/tmp/appsignal.log'
  File.open(file, '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'
  File.open(file, '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.

Go back

Subscribe to

Ruby Magic

Magicians never share their secrets. But we do. Sign up for our Ruby Magic email series and receive deep insights about garbage collection, memory allocation, concurrency and much more.