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

  raise MyError
  puts "This won't rescue MyError"

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

rescue Exception => error
  puts "Rescue #{error}"

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.