Why You Should Never Rescue Exception in Ruby
tl;dr Don't write rescue Exception => e
. Write rescue => e
or better still, figure out exactly what you're trying to rescue and use rescue OneError, AnotherError => e
.
What’s the deal?
A common pattern for rescuing exceptions in Ruby is:
def do_some_job!
# ... do something ...
job_succeeded
rescue
job_failed
end
This is fine, but when developers need to capture the exception details, a terrible, terrible thing happens:
def do_some_job!
# ... do something ...
job_succeeded
rescue Exception => e
job_failed e
end
I have been caught out by that code on at least three separate occasions. Twice when I wrote it. I write this post in the hope that I (and perhaps others) will finally wise up about exception handling and that my fingers will never, ever type that code again.
Just to confirm this is a actually bad practice, here’s ~200k results for rescue Exception =>
on Github
What is this I don’t even…
Exception
is the root of the exception class hierarchy in Ruby. Everything from signal handling to memory errors will raise a subclass of Exception. Here’s the full list of exceptions from ruby-core that we’ll inadvertently rescue when rescuing Exception.
SystemStackError
NoMemoryError
SecurityError
ScriptError
NotImplementedError
LoadError
Gem::LoadError
SyntaxError
SignalException
Interrupt
SystemExit
Gem::SystemExitException
Do you really want to rescue a NoMemoryError
and send an email saying the job failed?!? Good luck with that.
Better: Rescue StandardError
rescue => e
is shorthand for rescue StandardError => e
and is almost certainly the broadest type of Exception that we want to rescue. In almost every circumstance, we can replace rescue Exception => e
with rescue => e
and be better off for it. The only time when that’s not a good idea is for code that’s doing some kind of exception logging/reporting/management. In those rare cases, it’s possible we’ll want to rescue non-StandardErrors — but we still need to think pretty hard about what happens after we’ve rescued them.
Most of the time though, we don’t even want to rescue StandardError!
More Self-Inflicted Fail
Imagine a scenario where we’re connecting to a 3rd-party API in our application. For example, we want our users to upload their cat photos to twitfaceagram. We definitely want to handle the scenarios where the connection times out, or the DNS fails to resolve, or the API returns bogus data. In these circumstances, we want to present a friendly message to the user that the application couldn’t connect to the remote server.
def upload_to_twitfaceagram
# ... do something ...
rescue => e
flash[:error] = "The internet broke"
end
Most of the time, this code will do what we expect. Something out of our control will go wrong, and it’s appropriate to present the user with a friendly message. However, there’s a major gotcha with this code: we’re still rescuing many exceptions we’re not aware of.
Here’s an abridged list of StandardErrors defined in ruby-core 2.0.0 (1.9 is not materially different):
StandardError
FiberError
ThreadError
IndexError
StopIteration
KeyError
Math::DomainError
LocalJumpError
IOError
EOFError
EncodingError
Encoding::ConverterNotFoundError
Encoding::InvalidByteSequenceError
Encoding::UndefinedConversionError
Encoding::CompatibilityError
RegexpError
SystemCallError
Errno::ERPCMISMATCH
# ... lots of system call errors ...
Errno::NOERROR # errrr.... what?
RangeError
FloatDomainError
ZeroDivisionError
RuntimeError
Gem::Exception
# ... lots of gem errors ...
NameError
NoMethodError
ArgumentError
Gem::Requirement::BadRequirementError
TypeError
In a fresh Rails 3.2.13 application, there are 375 StandardErrors defined.
Now let’s say we’re refactoring the API integration and we make a typo with a method name. What’s going to happen?
If we’ve wrapped the entire process in a rescue => e
(which is rescuing StandardError) the NoMethodError is going to be swallowed and our graceful error handling code is going to be run instead. When we run our well written tests, they’ll fail. But rather than raising a straight-forward NoMethodError, it’ll look like there was an gracefully handled connectivity problem.
Now that is going to take some debugging.
If our tests are poorly written there’ll be no exception and perhaps the tests will just pass. Granted, in production our users won’t be seeing ugly 500 errors, but they sure won’t be uploading their cat photos either.
Best: Rescue Specific Exceptions
Every part of our code is qualified to rescue from certain exceptional circumstances. If we want to catch connectivity problems in an API integration, our code will be qualified to rescue from a long list of Net related exceptions. It is not qualified to rescue from an ArgumentError, which is a code-time problem and not a run-time problem!
Every time we write a rescue, we need to think hard about what exceptions this code is actually qualified to handle.
In the case of HTTP, we can make it easier on ourselves and use a wrapper like faraday. In this case we’ll have a much shorter list of possible exceptions to rescue.
So…
… if you encounter rescue Exception => e
in an existing codebase, you can almost certainly replace it with rescue => e
.
… if you find yourself about to type rescue Exception => e
, slap yourself in the face, figure out exactly what exceptions you’re dealing with and rescue those instead.