All topics
exceptions
beginner

Custom Exceptions & Chaining

Define domain-specific exception types and preserve the original cause when wrapping lower-level failures.

A custom exception is an exception class you write yourself, such as InsufficientFundsException, instead of reusing a general one like IllegalArgumentException.

A receipt that says "delivery failed — caused by: truck breakdown — caused by: engine fault". Each layer adds context without losing the root cause.

Key Concepts

1
Chaining means passing the original exception into the new one, so the log shows both what failed and why.
2
Write one when a caller needs to tell it apart. If every caller treats your new exception exactly like IllegalArgumentException, the extra class earns nothing. It pays when a caller can catch it specifically — retry on AccountFrozenException, give up on InsufficientFundsException — or when it carries data a message cannot.
IllegalArgumentExceptionAccountFrozenExceptionInsufficientFundsException
3
Extend RuntimeException unless you have a reason not to. Application errors usually pass through several layers. A checked exception forces every one of them to declare or wrap it, and you end up with throws lists nobody reads.
RuntimeExceptionthrows
4
Always pass the cause. This is the one that matters.
    // the log says what failed, not why
    catch (SQLException e) {
        throw new DataAccessException("query failed");
    }
5
    // the log shows the whole chain
    catch (SQLException e) {
        throw new DataAccessException("query failed for user " + id, e);
    }
6
With the cause, the log reads:
7
    ServiceException: failed to load user 4821
    Caused by: SQLException: connection pool exhausted
    Caused by: SocketTimeoutException: connect timed out
8
Three layers, one incident, and you know where to look. Drop the cause and only the first line survives.
9
Put details in fields, not in the message. If the handler has to pull an account number out of a string with a regular expression, the design is wrong. Give the exception getAccountId() and getShortfall() instead.
getAccountId()getShortfall()
10
Keep the hierarchy shallow. One base type per area — PaymentException — with two or three specific ones under it. Callers can then catch broadly or precisely. Deep trees make people guess which class to catch.
PaymentException
11
What the interviewer is probing.1. "When do you write one?" Probing: judgement. Stalls: "For every error." Moves up: when a caller treats it differently, or when it carries data a message cannot.
12
2. "Checked or unchecked?" Probing: reasoning. Stalls: "Checked is safer." Moves up: unchecked for errors crossing layers; checked when the immediate caller really can recover.
13
3. "What must every custom exception have?" Probing: the cause. Stalls: "A message." Moves up: a (String, Throwable) constructor, and actually using it.
(String, Throwable)
14
4. "How does the handler get the details?" Probing: structure over strings. Stalls: "From the message." Moves up: typed fields with getters, so nobody parses text.
15
5. "How would you make one cheap?" Probing: where the cost is. Stalls: "They are already cheap." Moves up: capturing the stack trace is the expensive part; you can skip it for an expected, high-frequency signal, and you lose the trace everywhere else.
16
  A custom exception worth having
17
    public class InsufficientFundsException extends RuntimeException {
        private final String accountId;
        private final long shortfallMinor;   // paise, not double
18
        public InsufficientFundsException(String accountId,
                                          long shortfallMinor,
                                          Throwable cause) {
            super("Account " + accountId + " short by "
                  + shortfallMinor, cause);      // cause passed
            this.accountId = accountId;
            this.shortfallMinor = shortfallMinor;
        }
        public String getAccountId()   { return accountId; }
        public long getShortfallMinor(){ return shortfallMinor; }
    }
19
  The four constructors to provide
    (String message)
    (String message, Throwable cause)     <- the important one
    (Throwable cause)
    ()                                    optional
20
  Chained trace, what the log shows
21
    ServiceException: failed to load user 4821
      at UserService.load(UserService.java:44)
    Caused by: SQLException: connection pool exhausted
      at HikariPool.getConnection(HikariPool.java:190)
    Caused by: SocketTimeoutException: connect timed out
      at ...
22
    -> three layers, one incident, immediately diagnosable.
       Drop the cause and only the first line survives.
23
  Hot-path optimisation, used deliberately
24
    @Override
    public synchronized Throwable fillInStackTrace() {
        return this;          // no stack capture: ~50x cheaper
    }
    // justified for an expected, high-frequency control signal;
    // never for an exception that reports a real fault.