Custom Exception Hierarchy Best Practices

Feb 18, 2021

When a software engineer designs and implements an application, they try to think of anything that could go wrong and handle it appropriately. The programming language they use offers special tools and constructs to help the developers achieve this goal.

In object oriented programming, an exception is being used to model an error that occurs at runtime. When things don't go as expected or assumptions are not valid, then a piece of code might decide to raise an exception. The exception raised notifies the caller about the error and lets it handle it. The caller might decide to not handle the error. In that case, the error is bubbled up to the next higher caller in the stack. This continues until the error reaches the top level of the call stack. At that level, if the error is not handled, then the program usually stops and reports the error to the operating system.

Built-In Exception Classes

All the popular object oriented programming languages come with an already defined set of exception types (or classes). They use them to report errors to the program. For example, in Ruby, there is an exception class called KeyError, which is raised when the program is trying to access a position in hash, using a key that does not exist. Here it is how you can see this exception being raised:

{foo: 'bar'}.fetch('mary')
KeyError: key not found: "mary"
from (irb):6:in `fetch'
        from (irb):6
        from /...bin/irb:11:in `<main>'

The Ruby built-in exception classes can be found here. As we said, all popular OO programming languages have such a hierarchy. For example, you can read about Python and Java here:

Exception Hierarchy

What is common in the way programming languages model their different exception classes is the exception class hierarchy. I.e. they define a root exception class and they make all the others being subclasses of the root superclass.

For example, in Ruby, the root exception superclass is Exception. In Python, it is called BaseException and in Java, it is called Throwable.

Not only that, as you can see, the built-in exception classes are not all immediate subclasses of the root superclass. The tree has multiple levels. For example, the class ZeroDivisionError in Ruby is a subclass of StandardError which is a subclass of the root Exception.

ZeroDivisionError -|> StandardError -|> Exception.

Similarly, in Python, the same class ZeroDivisionError is subclass of ArithmeticError, which is a subclass of Exception, which is a subclass of the root BaseException.

ZeroDivisionError -|> ArithmeticError -|> Exception -|> BaseException.

This hierarchy groups different exception classes within different groups. Not only that, taking advantage of the inheritance property of the object oriented programming, an instance of an exception X is also an instance of any superclass exception of X. As an example, an instance of the exception ZeroDivisionError in Ruby, is also considered to be an instance of StandardError.

Handling Errors

The fact that an exception X is also an exception of any of its superclasses, allows the piece of code that wants to handle errors to depend on the superclass rather than on the specific class X.

Here is a code skeleton in Ruby, that would make this more clear:

  #... a piece of code that can raise any StandardError exception...
rescue StandardError => ex
  puts "Something went wrong. Here it is what: #{ex.message}"

The above rescue block is handling any error that is an instance of StandardError or any of its subclasses. For example, it would handle any ZeroDivisionError which is thrown when we try to divide by 0. Try this out:

  1 / 0
rescue StandardError => ex
  puts "Something went wrong. Here it is what: #{ex.message}"

It will print:

Something went wrong. Here it is what: divided by 0

Or it would handle KeyError instances because they are subclasses of StandardError too:

KeyError -|> IndexError -|> StandardError -|> Exception.

Here is an example of it:

  {foo: 'bar'}.fetch('mary')
rescue StandardError => ex
  puts 'Something went wrong. Here it is what: key not found: "mary"'

This is really convenient, isn't it? The client code that is handling errors simply rescues (or catches) the superclass and, thanks to the inheritance property of objects, this make it handle any errors of that class or any of its subclasses. If ZeroDivisionError and KeyError were not subclasses of StandardError, but they were standalone classes, then we would have to write something like these:

  # ....
rescue ZeroDivisionError, KeyError => ex
  puts "Something went wrong. Here it is what: #{ex.message}"

The longer is the list of exceptions we want to handle, the longer this rescue statement becomes, if these exceptions do not belong to the same hierarchy branch.

Custom Exception Classes

Hence, as we can see above, the designers of the programming languages have done very good job to design a hierarchy that makes it easy for us to rescue/catch errors, without having to explicitly name each one of the possible error classes.

This pattern needs to be followed, as a good practice, for all the custom exception classes we design for our application. I.e. our exception classes should be grouped into groups and should belong to a superclass that would make it convenient for other parts of the application code to rescue and handle errors.

In order to be more specific, when you design a class that might raise a custom exception, do it following this pattern here:

class X
  class Error < StandardError; end
  class Error1 < Error; end
  class Error2 < Error; end
  class Error3 < Error; end
  class Error4 < Error; end
  def something!
    # this part might raise any error: Error1, Error2, Error3, Error4 ....

As you can see there is a root exception definition, X::Error, that derives from StandardError, and then all the errors that might be raised are subclasses of it:

X::Error1 -|> X::Error -|> StandardError

X::Error2 -|> X::Error

X::Error3 -|> X::Error

X::Error4 -|> X::Error

Then, the client code can be:

rescue X::Error => ex
  # handle any error from class X

If we don’t use this technique, then the client code that wants to handle any / all errors from class X, will have to write something like:

rescue X::Error1, X::Error2, ... => ex
  # do something with any error from X

And not only that, when class X is amended to raise one more error, client code wouldn’t have it in its list, which would not allow it to handle it as part of any error from class X.

Hence, the technique makes our class client-code friendly.

Stripe - An Example Of A Popular Library Using This Technique

This best practice is followed by many popular libraries. One of them is Stripe.

The commands to Stripe API can raise numerous different exceptions. With the way Stripe has designed their exception hierarchy, we only have to do:

rescue ::Stripe::StripeError => ex
  # handle charge card errors or any exception class that might be raised.

in order to handle Stripe errors (when we don’t want to handle a specific one). This is doable, because all the Stripe Error exception classes derive from Stripe::StripeError class. See here.

Closing Note

This was about designing your custom exception classes into a hierarchy of classes that would allow the client code using your class to easily handle any errors that your class might throw. This is a practice that is followed by many popular libraries and it is supported in any object oriented programming language thanks to the inheritance property.