Custom Exceptions in Java

A custom exception names a failure in the language of your domain, which makes handling precise and messages meaningful.

Why write your own

  • The type itself carries meaning: InsufficientBalanceException says more than IllegalStateException.
  • Callers can catch exactly the failure they know how to handle.
  • Extra data can travel with the exception, such as the account and the shortfall.
  • A common base type lets one handler cover a whole subsystem.

An unchecked custom exception

public class InsufficientBalanceException extends RuntimeException {

    private final String accountId;
    private final long shortfall;

    public InsufficientBalanceException(String accountId, long shortfall) {
        super("Account " + accountId + " is short by " + shortfall);
        this.accountId = accountId;
        this.shortfall = shortfall;
    }

    public String accountId() {
        return accountId;
    }

    public long shortfall() {
        return shortfall;
    }
}
public void withdraw(long amount) {
    if (amount > balance) {
        throw new InsufficientBalanceException(id, amount - balance);
    }
    balance -= amount;
}
try {
    account.withdraw(5000);
} catch (InsufficientBalanceException e) {
    System.out.println("Add " + e.shortfall() + " to continue");   // useful detail
}

A checked custom exception

public class NoteNotFoundException extends Exception {

    private final long noteId;

    public NoteNotFoundException(long noteId) {
        super("No note with id " + noteId);
        this.noteId = noteId;
    }

    public NoteNotFoundException(long noteId, Throwable cause) {
        super("No note with id " + noteId, cause);      // always offer this form
        this.noteId = noteId;
    }

    public long noteId() {
        return noteId;
    }
}

Extend Exception for checked, RuntimeException for unchecked. That single choice is the whole difference.

Which one to choose

Choose checked whenChoose unchecked when
The caller can realistically recoverThe failure means a bug
The failure is an expected external conditionAn argument or state was invalid
You want the compiler to force a decisionHandling would be pointless boilerplate
Modern Java leans towards unchecked. Before adding a checked exception, ask what a caller would actually do differently on catching it. If the honest answer is "log it and give up", unchecked is the better choice.

A base type for a subsystem

public class StorageException extends RuntimeException {
    public StorageException(String message, Throwable cause) {
        super(message, cause);
    }
}

public class RecordNotFoundException extends StorageException {
    public RecordNotFoundException(String message) {
        super(message, null);
    }
}

public class DuplicateKeyException extends StorageException {
    public DuplicateKeyException(String message, Throwable cause) {
        super(message, cause);
    }
}
try {
    repository.save(note);
} catch (DuplicateKeyException e) {
    return conflict(e.getMessage());     // a specific reaction
} catch (StorageException e) {
    return serverError(e);               // everything else from storage
}

Translating low level exceptions

public Note find(long id) {
    try {
        return jdbc.queryForNote(id);
    } catch (SQLException e) {
        throw new StorageException("Lookup failed for note " + id, e);
    }
}

Callers should depend on your abstraction, not on the fact that storage happens to be SQL today. Wrapping keeps the boundary clean, and the cause keeps the diagnosis.

The four constructors to offer

public class ValidationException extends RuntimeException {
    public ValidationException() { }
    public ValidationException(String message) { super(message); }
    public ValidationException(String message, Throwable cause) { super(message, cause); }
    public ValidationException(Throwable cause) { super(cause); }
}

Provide at least the message and the message plus cause forms. An exception with no way to attach a cause forces callers to discard information.

Carrying structured data

public class ValidationException extends RuntimeException {

    private final List<String> problems;

    public ValidationException(List<String> problems) {
        super("Validation failed: " + problems.size() + " problem(s)");
        this.problems = List.copyOf(problems);
    }

    public List<String> problems() {
        return problems;
    }
}

This is a real advantage of a custom type. The handler gets the list of problems as data, rather than having to parse a message string.

Common mistakes

  • Creating a new exception type for every method, producing dozens that nobody catches individually.
  • Extending Exception by reflex, which forces throws everywhere for no benefit.
  • Omitting the constructor that accepts a cause.
  • Putting secrets such as passwords or tokens into the message.
  • Extending Throwable or Error directly.
  • Naming a class without the Exception suffix, which breaks a very strong convention.

Best practices

  • Add a custom exception only when a caller would treat it differently.
  • Name it after the problem and end the name with Exception.
  • Prefer unchecked unless recovery is genuinely expected.
  • Give a subsystem one base type and a small number of subtypes.
  • Include identifying data as fields, not only in the message.
  • Make the exception immutable.

Practice

  1. Write InvalidCouponException carrying the coupon code, and catch it to print a specific message.
  2. Decide checked or unchecked for: a malformed configuration file, a negative quantity, a network timeout. Justify each.
  3. Build a base exception with two subtypes and a handler that treats one specially.
  4. Why should every custom exception offer a constructor taking a cause?
  5. Convert a method that returns an error code into one that throws a custom exception, and compare the call sites.

Conclusion

Write a custom exception when the type itself communicates something a caller can act on. Keep the hierarchy small, prefer unchecked, always support a cause, and carry the useful details as fields.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Java notes →

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.