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.
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.
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.
The error message says it couldn't read the config file, but the real problem was a typo in the code.
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.
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.
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.
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) #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.
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.
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.
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.