How do you rethrow a caught exception in Java while preserving its original stack trace, and what is the difference between rethrowing the same instance and wrapping it in a new exception?
answer
- Stack trace filled in at construction (fillInStackTrace), not at throw
- throw e; = same object = trace preserved
- new X(e.getMessage()) destroys cause + origin
- Wrap with cause: new X(msg, e) → 'Caused by:'
- getCause / getSuppressed retrieve linked throwables
basics
~20 sUse throw e; to rethrow the exact exception you caught. Because it is the same object, its stack trace stays intact. If you wrap it in a new exception, pass the original as the cause so the trace is not lost.
solid answer
~40 sTo preserve the original stack trace, rethrow the *same* caught instance with `throw e;`. A Java exception captures its stack trace once, when it is constructed (in `fillInStackTrace()`), so as long as you throw that same object the trace is unchanged. The common mistake is `throw new RuntimeException(e.getMessage());` — that creates a brand-new exception whose trace starts at the rethrow point and drops the original cause. If you need to convert the type, always wrap with the cause: `throw new ServiceException("...", e);`. The original is then reachable via `getCause()` and printed as a 'Caused by:' section. You can also enrich a rethrow without losing context using `addSuppressed` or `initCause`. The guiding rule: never discard the original throwable's stack trace or cause.
code
java · 20 lines// GOOD: rethrow same instance — trace preserved
try {
parseConfig();
} catch (IOException e) {
log.warn("config read failed, will retry");
throw e; // same object, original origin kept
}
// GOOD: type translation with chaining
try {
jdbc.query(sql);
} catch (SQLException e) {
throw new DataAccessException("query failed", e); // e = cause
}
// BAD: destroys origin and cause
try {
jdbc.query(sql);
} catch (SQLException e) {
throw new RuntimeException(e.getMessage()); // trace lostgo deeper
Knows to write throw e; to rethrow and that new RuntimeException(e.getMessage()) loses information; can read a 'Caused by:' section.
Explains that the trace is captured at construction, wraps with the cause when translating types, and knows getCause/getSuppressed.
Designs exception-translation boundaries, never drops traces, uses suppressed exceptions correctly, and reasons about double-logging anti-patterns.
Sets team-wide conventions for exception chaining and logging-at-the-boundary, weighs performance of fillInStackTrace, and considers writableStackTrace=false for control-flow-style exceptions.
## What is an exception and a stack trace? In Java, an **exception** is an object representing an error or abnormal condition. When code does `throw someException`, normal execution stops and the JVM looks up the call stack for a matching `catch` block. Every exception is a subclass of `java.lang.Throwable`. A **stack trace** is the recorded list of method calls that were active at the moment the exception was created — e.g. `main → service() → repository() → parse()`. It is what you see printed as lines like `at com.app.Foo.bar(Foo.java:42)`. It is the single most valuable piece of debugging information because it tells you *where* the problem originated. ## When is the stack trace captured? Crucially, the stack trace is captured **at construction time**, not at throw time. `Throwable`'s constructor calls a method `fillInStackTrace()` that snapshots the current call stack into the object. This means: - The trace reflects where `new SomeException(...)` ran, not where `throw` ran (they are usually the same line, so the distinction rarely matters). - Once captured, the trace lives inside that object and does not change, no matter how many times you rethrow that *same* object. ## Rethrowing the same instance (preserves the trace) ```java try { risky(); } catch (IOException e) { log.warn("retrying", e); throw e; // same object → original trace intact } ``` Because `e` is the identical object that was originally created deep in the call stack, its `fillInStackTrace` snapshot is untouched. The caller sees the full original trace. ## The classic mistake: creating a new exception ```java catch (IOException e) { throw new RuntimeException(e.getMessage()); // BAD } ``` This constructs a **brand-new** exception. Its stack trace is filled in *here*, so it begins at this catch block — everything below (the real origin) is gone. Worse, only the message string was copied; the original throwable is not linked, so you cannot recover the real cause. This is the number-one way developers destroy debugging information. ## The correct conversion: wrap with the cause When you legitimately need to change the exception type (e.g. translate a checked `SQLException` into your own `DataAccessException`), wrap it and pass the original as the **cause**: ```java catch (SQLException e) { throw new DataAccessException("load failed", e); // e becomes the cause } ``` `Throwable` has a `cause` field (set via the two-arg constructor or `initCause(e)`). When printed, you get the new exception's trace followed by a section: ``` Caused by: java.sql.SQLException: ... at ... ``` so both traces survive. `getCause()` retrieves the original programmatically. This is **exception chaining**. ## Suppressed exceptions There is a third relationship: **suppressed** exceptions, used mainly by try-with-resources. If the body throws and a `close()` also throws, the close exception is attached to the primary one via `addSuppressed`, retrievable with `getSuppressed()`. None of the traces are lost. ## Summary rule Never throw away a throwable's stack trace or cause. Either rethrow the same instance (`throw e;`) or wrap-with-cause (`new X(msg, e)`). The only thing you must never do is `throw new X(e.getMessage())`.
- What happens to the stack trace if you call e.fillInStackTrace() again before rethrowing?It re-snapshots the stack to the current location, overwriting the original origin — so you lose the deep trace. You almost never want to do this on a caught exception.
- How are suppressed exceptions different from a cause?A cause is the underlying reason for an exception (one chained 'Caused by'). Suppressed exceptions are secondary throwables that occurred while handling the primary one (e.g. a failing close() in try-with-resources), attached via addSuppressed and shown as 'Suppressed:'.
saying these in an interview costs you the question
- Thinking `throw new RuntimeException(e.getMessage())` keeps the original trace
- Believing the trace is captured at throw time rather than construction time
- Logging the exception AND rethrowing it, causing the same error to be logged twice
- Calling printStackTrace() and then swallowing the exception instead of propagating