So, you’re about to define a new exception for your Ruby application. What class should it
inherit from? The most likely candidates are StandardError
, RuntimeError
and Exception
. I
think StandardError
is the best choice and I’ll explain why.
Let’s start with why you should never pick Exception
.
When declaring a rescue
block, you can specify an exception class to handle. This uses is_a?
to check for a match when an exception is raised, so the block applies to subclasses too.
The default class for rescue
is StandardError
. So exceptions that inherit directly from
Exception
will pass through basic rescue blocks.
class MyError < Exception; end
begin
raise MyError
rescue
puts "This won't rescue MyError"
end
You might try to solve this by rescuing Exception
, but this introduces a worse problem.
Because Exception
is the base error class it is also parent other errors you probably don’t
want to handle. For example, untrapped signals raise a SignalException
. If you do not re-raise
these, your app may not shut down properly.
Another example is SystemStackError
. Rescuing this prevents your application from crashing
when a stack overflow happens. This might sound good, but it can cause your app to exit cleanly,
with no stacktrace to explain the failure. For a web app it could lead to mysteriously dropped
requests with no way to detect failure.
def boop
boop
end
begin
boop
rescue Exception => error
puts "Rescue #{error}"
end
If you actually want to prevent stack overflows from crashing the app, you should be explicit and shouldn’t rely on a rescue wrapping your entire application.
Now, what about RuntimeError
? This is better because it extends from StandardError
so
there’s no surprises with rescue blocks. But there is still a subtle reason to prefer
StandardError
.
The default exception class for raise
is RuntimeError
. This makes it simple to raise a
one-off message with just a string. By extending from RuntimeError
the meaning of your custom
error gets blurred with this concept.
There is already an example of this in Ruby, where attempting to modify a frozen object raises
a FrozenError
, which extends from RuntimeError
. This was done to preserve the
original behaviour of raising a RuntimeError
, which was ambiguous. If FrozenError
had already been defined using StandardError
this ambiguity wouldn’t exist. There are other
examples of this in many common Ruby libraries, like Rails and FactoryBot.
For these reasons, I think StandardError
is the best choice. It doesn’t introduce surprising
behaviour and avoids ambiguity, which is really the point of defining a new exception class.
For more details about Ruby’s exception system, I highly recommend Exceptional Ruby by Avdi Grimm, it’s a brisk and insightful read that is essential for every Ruby programmer.