skip to content

When catching and rethrowing an exception in Java, how do you preserve the original stack trace and cause, and what goes wrong if you don't?

level: middleimportance: must knowfreq 72%

answer

  1. new X(msg, cause) chaining constructor
  2. Caused by: chain → root cause
  3. wrapping without cause = trace lost
  4. rethrow / don't catch = trace untouched
  5. log once at the boundary, never swallow

basics

~20 s

Pass the original exception as the cause when you wrap it: throw new MyException("msg", original). If you only do throw new MyException("msg") you throw away the original's stack trace and message, so you lose where the real problem happened.

solid answer

~50 s

The original exception carries a stack trace pointing at the real failure and possibly its own cause chain. To keep that when translating to a different exception type, use the chaining constructor: throw new ServiceException("loading user", e). The new exception then has e as its cause, and printStackTrace / loggers print the full "Caused by:" chain down to the root. The two ways to lose information are: (1) creating a new exception without passing the cause — the original trace is gone; and (2) catching and rethrowing a generic message that drops the type and details. If you simply want to propagate the same exception, just rethrow it (throw e) or do not catch it at all; that keeps the original trace untouched. Avoid catch-log-and-swallow and avoid logging then rethrowing the same exception, which double-logs. The goal is one place that records the full chain so diagnosis can follow it to the root cause.

code

java · 15 lines
java
// WRONG: original cause discarded
catch (SQLException e) {
    throw new RepositoryException("load user failed"); // trace starts here only
}

// RIGHT: chain the cause so "Caused by:" shows the SQLException trace
catch (SQLException e) {
    throw new RepositoryException("load user failed", e);
}

// RIGHT: just propagate the same type unchanged
catch (SQLException e) {
    metrics.increment("db.error");
    throw e; // original trace preserved
}

go deeper

for a junior

Knows to pass the original exception as the second constructor argument so the cause is kept, and not to throw away exceptions silently.

for a middle

Explains the cause chain and 'Caused by:' output, the wrong-vs-right wrapping patterns, and rethrow/propagate to keep the trace untouched.

for a senior

Discusses exception translation across layers, more-precise rethrow (Java 7), log-once-at-the-boundary discipline, and the cause vs suppressed distinction.

for a principal

Sets organization-wide conventions: where translation happens, structured logging of the full chain, avoiding leakage of low-level types across APIs, and observability so root causes are always recoverable from logs/traces.

## What a stack trace and a cause are When an exception is created, the JVM captures a **stack trace**: the list of method calls (with file and line numbers) that were active at that moment. This is the single most valuable piece of debugging information — it tells you *where* the failure originated. An exception can also have a **cause**: another `Throwable` that triggered it. This forms a **cause chain**. When you print the exception, the JVM shows the top exception's trace, then `Caused by: ...` followed by the next exception's trace, and so on down to the **root cause** — the deepest, original failure. Following that chain is how you find the real problem. ## Exception translation (wrapping) It is common and good practice to **translate** a low-level exception into one that fits your layer's abstraction — for example, turning a `SQLException` into a domain `RepositoryException` so callers do not depend on JDBC details. The danger is doing this in a way that **discards the original trace**. ### The wrong way (loses the cause) ```java try { jdbc.query(...); } catch (SQLException e) { throw new RepositoryException("could not load user"); // BUG: e is dropped } ``` Here the new exception is created with no cause. Its stack trace starts at *this* catch block, not at the JDBC call. The original message and trace from `e` are gone forever. Anyone debugging sees "could not load user" with no hint of *why* (timeout? bad SQL? connection refused?). ### The right way (chains the cause) ```java catch (SQLException e) { throw new RepositoryException("could not load user", e); // e becomes the cause } ``` Every well-behaved exception class offers a constructor taking `(String message, Throwable cause)` — this is the convention from `Throwable`. Now printing the `RepositoryException` shows your message *and* `Caused by: java.sql.SQLException: ...` with the original trace. If your custom exception lacks such a constructor, you can also call `initCause(e)` once. ## Just rethrowing the same exception If you do not need to change the type, the cleanest options are: - **Do not catch it** — let it propagate; the trace is untouched. - **Rethrow it**: `throw e;` — also untouched. Since Java 7, rethrowing in a multi-catch or with a precise declared type works without forcing you to widen the method's `throws` clause ("more precise rethrow"). Never do `throw new RuntimeException(e.getMessage())` just to propagate — that drops both the type and the cause. ## Suppressed exceptions (a related concept) When a try-with-resources block's body throws and a `close()` also throws, the close exception is added as a **suppressed** exception on the primary one (`getSuppressed()`), so it is preserved rather than overwriting the real failure. This is the same theme: never silently discard a throwable. ## Anti-patterns to avoid - **Catch, log, swallow**: `catch (Exception e) { log.error("oops"); }` — hides the failure and continues in a broken state. - **Log-and-rethrow the same exception**: causes the same error to be logged multiple times up the stack; log it **once**, at the boundary that actually handles it. - **`e.printStackTrace()` in production code**: writes to stderr, bypassing your logging configuration. - **Catching `Throwable`/`Exception` too broadly** then losing the specific type. ## The principle Never throw away a `Throwable`. Either let it propagate, rethrow it, or wrap it **with the original as the cause**. Preserve one complete chain so the root cause is always reachable from the logs.

  • How do you read the underlying cause when debugging from a stack trace?
    Follow the 'Caused by:' lines downward; the last (deepest) one is the root cause. Programmatically, walk getCause() until it returns null.
  • What is the difference between a cause and a suppressed exception?
    A cause is what triggered this exception (the chain via getCause). A suppressed exception is a secondary exception that occurred while handling the primary one — typically a failing close() in try-with-resources — preserved via getSuppressed() instead of overwriting the primary.

An exception's cause chain is like a paper trail of receipts. Wrapping without the cause is like writing 'payment failed' on a fresh sticky note and shredding the original receipt — you keep the complaint but destroy the evidence of what actually went wrong.

saying these in an interview costs you the question

  • Wrapping with new RuntimeException(e.getMessage()) — drops the cause and the type.
  • Saying 'I rethrow with a new exception and the trace is kept automatically' — only if you pass the cause.
  • Logging the exception and then rethrowing it (double logging up the stack).
  • Catching and swallowing to 'keep the app running' without recording or rethrowing.

context