What are best practices for designing custom exceptions?

8 minintermediatecustom-exceptionsbest-practicesdesign

Quick Answer

Extend the most specific existing exception class that fits (usually RuntimeException for most modern APIs, unless the failure is truly recoverable and callers benefit from being forced to handle it). Provide constructors that accept a message and an optional cause for chaining, keep the exception name descriptive of the failure, include relevant context as fields rather than only in the message string, and avoid catching and swallowing exceptions just to satisfy the compiler.

Detailed Answer

A few practical guidelines that come up repeatedly in code review:

  1. Choose checked vs unchecked deliberately. Extend Exception only if callers genuinely have a meaningful, expected recovery path and should be forced to consider it; otherwise extend RuntimeException. Most modern library design (Spring's DataAccessException hierarchy, for example) favors unchecked exceptions even for what feels like a "recoverable" condition, to avoid throws clauses cascading through unrelated layers.

  2. Always provide a cause-preserving constructor:

public class OrderProcessingException extends RuntimeException {
    public OrderProcessingException(String message) { super(message); }
    public OrderProcessingException(String message, Throwable cause) { super(message, cause); }
}
  1. Name it after the failure, not the mechanismInsufficientFundsException communicates more than AccountOperationException.

  2. Carry structured context as fields, not just a formatted message string, so calling code (and logging) can act on specific data without parsing text:

public class InsufficientFundsException extends RuntimeException {
    private final BigDecimal shortfall;
    public InsufficientFundsException(BigDecimal shortfall) {
        super("Insufficient funds: short by " + shortfall);
        this.shortfall = shortfall;
    }
    public BigDecimal getShortfall() { return shortfall; }
}
  1. Never swallow exceptions silently (catch (Exception e) {}) just to satisfy the compiler on a checked exception — at minimum log it, or rethrow as an unchecked, chained exception.

  2. Don't create an exception hierarchy deeper than needed — a flat set of specific exception types is usually easier to reason about than a deep inheritance tree mirroring your domain model.