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?
answer
- new X(msg, cause) chaining constructor
- Caused by: chain → root cause
- wrapping without cause = trace lost
- rethrow / don't catch = trace untouched
- log once at the boundary, never swallow
basics
~20 sPass 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 sThe 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// 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
Knows to pass the original exception as the second constructor argument so the cause is kept, and not to throw away exceptions silently.
Explains the cause chain and 'Caused by:' output, the wrong-vs-right wrapping patterns, and rethrow/propagate to keep the trace untouched.
Discusses exception translation across layers, more-precise rethrow (Java 7), log-once-at-the-boundary discipline, and the cause vs suppressed distinction.
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.