Custom Exceptions

Basic Java
≈ 3 min read

Why create your own exceptions

Java already has many built-in exceptions, but sometimes an error belongs specifically to the rules of your application. For example, instead of a generic RuntimeException, it is much clearer to see UserNotFoundException: the name itself tells you what happened.

A custom exception is an ordinary class that extends an appropriate exception class. The choice between Exception and RuntimeException follows the same checked and unchecked exception rules that we have already covered.

Creating a custom exception

Most exception classes do not need complex logic. It is usually enough to give the class a clear name and pass the message to the parent class constructor.

public class UserNotFoundException extends RuntimeException {

    public UserNotFoundException(String message) {
        super(message);
    }
}

Now this exception can be thrown just like any built-in exception:

public User findUser(long id) {
    User user = loadUser(id);

    if (user == null) {
        throw new UserNotFoundException("User not found: " + id);
    }

    return user;
}

Code that calls findUser() receives not an abstract “runtime error,” but a specific error type. It can be caught separately with catch if this situation requires a special response.

Checked or unchecked

A custom exception does not become checked or unchecked on its own — that is determined by its parent class. If the class extends Exception but not RuntimeException, the compiler requires that the exception be handled or declared. A subclass of RuntimeException remains unchecked.

public class InvalidReportException extends Exception {

    public InvalidReportException(String message) {
        super(message);
    }
}

That is why you first determine what the error means and only then choose the base class. Inheritance here affects not only the class's place in the hierarchy, but also how the compiler requires calling code to work with this exception.

An exception name is part of the program's meaning. PaymentRejectedException or UserNotFoundException usually says more about the problem than a generic RuntimeException with the same message.

The message and the cause

If a new exception is created on top of an error that has already occurred, it is useful to preserve the original cause. To do that, you can add a constructor with a Throwable cause parameter and pass it to super.

public class UserLoadingException extends RuntimeException {

    public UserLoadingException(String message, Throwable cause) {
        super(message, cause);
    }
}

This gives the new exception its own clear meaning without losing the original error: it can be obtained through getCause(), and the chain of causes is preserved in the stack trace.