skip to content

Exception Chaining

Wrapping a low-level failure while passing it as the cause keeps the original context, which is what produces the Caused by section of a stack trace. Losing the cause is one of the most common real-world logging mistakes, so interviewers ask about it.

part ofJavaoverview, primer and where to startread it →
on this pageshow

questions

5

What is exception chaining in Java, and why would you wrap one exception inside another?

level: juniorimportance: must knowfreq 70%

answer

  1. wrap low-level exception in a higher-level one
  2. pass original Throwable to constructor (the cause)
  3. getCause() retrieves it
  4. 'Caused by:' in the printed stack trace
  5. preserves root cause + clean API

basics

~20 s

Exception chaining means attaching the original exception (the cause) to a new exception when you wrap it. You pass the original to the new exception's constructor so you don't lose information about what really went wrong.

solid answer

~40 s

Exception chaining records the original exception as the 'cause' of a new one. When a low-level operation fails (say a SQLException) you often want to throw a more meaningful, higher-level exception that fits your API. If you just throw the new exception, you lose the original stack trace and message. Chaining preserves it: you pass the original Throwable into the new exception's constructor (e.g. new ServiceException("load failed", sqlEx)). The cause is stored on the Throwable and retrievable via getCause(). When the stack trace prints, you see both, joined by a 'Caused by:' section, so you can trace the failure from the abstract symptom down to the real root cause. This keeps your API clean while keeping debugging information intact.

code

java · 11 lines
java
try {
    user = jdbc.loadUser(id);
} catch (SQLException e) {
    // chain: keep the original SQLException as the cause
    throw new UserRepositoryException("could not load user " + id, e);
}

// elsewhere:
catch (UserRepositoryException ex) {
    Throwable root = ex.getCause(); // -> the original SQLException
}

go deeper

for a junior

Can state that chaining means passing the original exception into the new one's constructor so it isn't lost, and recognize 'Caused by:' in a stack trace.

for a middle

Explains the layering motivation (clean API vs. preserved root cause) and uses both constructor and initCause(), reading the chain via getCause().

for a senior

Frames chaining as exception translation across layers, knows when to wrap vs. propagate, and reasons about how the printed chain aids diagnosis.

for a principal

Sets team conventions for exception translation boundaries, ensures logging/monitoring surfaces the full cause chain, and avoids anti-patterns like swallowing causes or double-logging.

## The problem chaining solves In Java, an **exception** is an object representing an error or abnormal condition. The base class for all of them is **`Throwable`** (its two main subclasses are `Error` and `Exception`). You signal an error by `throw`-ing a Throwable; code higher up `catch`-es it. Now imagine your data-access code calls JDBC, which throws a low-level `SQLException` ("connection refused"). Your *service layer* doesn't want to expose database details to its callers — it wants to throw something meaningful in its own vocabulary, like a `UserRepositoryException`. So you **catch** the low-level exception and **throw** a higher-level one. This is called **exception wrapping** (or *translation*). The naive version loses information: ```java try { ... } catch (SQLException e) { throw new UserRepositoryException("could not load user"); // e is thrown away! } ``` The original `SQLException` — its message and its stack trace pointing at the exact failing line — is gone. When this blows up in production you only see "could not load user" with no clue *why*. ## What chaining is **Exception chaining** fixes this by recording the original exception as the **cause** of the new one. Every `Throwable` has an internal field for its cause. You set it in one of two ways: 1. **Via constructor** — most Throwable classes have a constructor taking a `Throwable cause`: ```java throw new UserRepositoryException("could not load user", e); ``` 2. **Via `initCause(Throwable)`** — for exceptions whose constructor doesn't accept a cause: ```java UserRepositoryException ex = new UserRepositoryException("could not load user"); ex.initCause(e); throw ex; ``` You later read it back with **`getCause()`**, which returns the cause Throwable (or `null` if none). ## What you see in the stack trace When a chained exception is printed (via `printStackTrace()` or by a logger), Java prints the top exception's stack trace, then a line starting with **`Caused by:`** followed by the cause's stack trace, recursively down the chain. Example: ``` UserRepositoryException: could not load user at com.app.UserRepo.load(UserRepo.java:42) ... Caused by: java.sql.SQLException: connection refused at com.app.Jdbc.connect(Jdbc.java:88) ... ``` This lets you read top-down: the *symptom* ("could not load user") and then the *root cause* ("connection refused"). To save space, repeated frames shared between the two traces are collapsed into a `... N more` line. ## Why it matters Chaining gives you the best of both worlds: a clean, layered API (callers see meaningful exceptions, not raw JDBC) **and** full diagnostic detail (the real root cause is never lost). It is the standard idiom whenever you translate an exception from one layer to another.

  • What happens to the original exception if you wrap without passing it as the cause?
    It is lost: the original message and stack trace are gone, so there is no 'Caused by:' section and you cannot tell the real root cause from the logs. This is a common debugging killer.
  • How do you retrieve the wrapped exception later?
    Call getCause() on the outer Throwable; it returns the cause (or null). You can walk the chain by calling getCause() repeatedly until it returns null.

Like a relay-race baton: the new exception carries the original one forward instead of dropping it, so whoever reads the final report can trace the failure back to where it actually started.

saying these in an interview costs you the question

  • Saying chaining means catching and re-throwing the same exception (that is rethrow, not chaining).
  • Thinking the cause is automatically captured — you must explicitly pass it via constructor or initCause().
  • Believing chaining merges the two stack traces into one trace instead of a 'Caused by:' section.

context

open as a page

What are the common mistakes when wrapping exceptions, and how do you wrap correctly to preserve the original context?

level: seniorimportance: must knowfreq 50%

basics

~20 s

The big mistakes are catching an exception and throwing a new one without passing the original as the cause, and logging plus rethrowing the same error (double logging). Always pass the caught exception as the cause and log it only once, at the boundary that handles it.

open as a page

How does a chained exception appear in a printed stack trace, and how do you read it to find the root cause?

level: juniorimportance: should knowfreq 60%

basics

~20 s

The outer exception's stack trace prints first, then a line 'Caused by:' followed by the cause's trace, repeating down the chain. The deepest 'Caused by:' is usually the real root cause. Read from the bottom up.

open as a page

What are the two ways to set the cause of an exception in Java, and when would you use initCause() instead of a cause constructor?

level: middleimportance: should knowfreq 55%

basics

~10 s

You can set the cause either by passing it to a constructor that takes a Throwable, or by calling initCause(theCause). Use initCause() when the exception class has no constructor that accepts a cause.

open as a page

How does exception chaining support translating checked exceptions to unchecked ones across architectural boundaries, and what are the trade-offs?

level: principalimportance: should knowfreq 35%

basics

~20 s

You catch a checked exception and throw an unchecked one (a RuntimeException subclass) with the original as the cause. This stops low-level checked exceptions from leaking through every method signature, while chaining keeps the root cause for debugging.

open as a page