Exceptions in API Design

What are best practices for designing APIs regarding exceptions?

Prefer Exceptions over Return Codes

Since methods should either change the internal state of an object or return a value, signaling an exception state via return code is bad practice. Use exceptions since they keep your API clean.

Document Exceptions with Javadoc

Every exception, checked or unchecked, should be precisely documented. The phrase starts with the word "if" as in

* ...
* @throws IllegalArgumentException if the given input value is empty or contains whitespaces.
* ...

Use own Exceptions in public API

If an exception is thrown by a sub module you use but have not in control, you should hesitate to put this exception into your API. This would make you dependent on the sub module and if the exception type thrown in this sub module changes, your API is also affected. Often the exception of the sub module is in another level of abstraction. Therefore you should catch that sub module's exception and create and throw one of your own. This will keep your API clean and rest you in control of it.

There are two forms of dealing with this

  1. Exception Translation
    The exception thrown by the sub module is caught, the information contained evaluated and a new exception is constructed and thrown. The stack trace of the sub module's exception is lost, but on the other side does not clutter around if no one will use it. In addition to that this will remove the danger that a specific exception is thrown to a tier that has no access to the exception's class file.
  2. Exception Chaining
    The exception thrown by the sub module is caught, a new exception is created with caught exception passed as the root cause. This new exception may or may not provide additional information on the exception case. This new exception is thrown.

    Chaining is often preferred over translation because the stack trace is often too valuable to be discarded.
    Note: If you want to provide a cause to an exception that provides no constructor passing the cause, please refer to Exception with no Cause.

Keep Number of thrown Exceptions small

Limit the number of exceptions you throw from a method. If the method handles many exceptions try to use Exception Translation or Exception Chaining and map them to one exception to be declared as thrown.

Declare thrown Subclass Exceptions

Although the the number of exceptions should be kept small, every exception should be declared. This adresses Subclasses of exceptions like java.io.FileNotFoundException that is a subclass of java.io.IOException.

public void execute() throws FileNotFoundException, IOException {
  ...
  }

This allows the client code to catch and handle exceptions individually. Without the declaration (and documentation) the developer has no chance but reading the source code to tell which exception are thrown by the method.

Declare thrown RuntimeExceptions

Although the the number of exceptions should be kept small, runtime exceptions should be declared and documented.

public void execute() throws IllegalArgumentException, MyAppRuntimeException {
  ...
  }

Without the documentation, the developer has no clue, which exceptions are thrown by the method, since only checked exceptions are required to be declared. The recommendation is to declare and document all runtime exceptions thrown by a method.

This does no extend to declaring eventual programming problems like NullPointerExceptions, but includes NullPointerExceptions if preconditions of a method are checked.

Mind the Level of Abstraction

A method should only throw exceptions on the same level of abstraction as the method itself. Methods on the presentation tier should normally not deal with java.io.IOException or java.sql.SQLException that bubble up from the resource tier. Instead the business tier should provide useful exceptions on the abstraction level of the business domain.

Especially do not mix exceptions of different abstraction levels in the throws clause of a method.