skip to content

Rethrowing and More-Precise Rethrow

Rethrowing the same instance preserves the original stack trace, and more-precise rethrow lets the compiler narrow what the method must declare even when you caught a broad type. Interviewers care most about not destroying the trace.

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

questions

4

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?

level: juniorimportance: must knowfreq 62%

answer

  1. Stack trace filled in at construction (fillInStackTrace), not at throw
  2. throw e; = same object = trace preserved
  3. new X(e.getMessage()) destroys cause + origin
  4. Wrap with cause: new X(msg, e) → 'Caused by:'
  5. getCause / getSuppressed retrieve linked throwables

basics

~20 s

Use 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 s

To 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
java
// 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 lost

go deeper

for a junior

Knows to write throw e; to rethrow and that new RuntimeException(e.getMessage()) loses information; can read a 'Caused by:' section.

for a middle

Explains that the trace is captured at construction, wraps with the cause when translating types, and knows getCause/getSuppressed.

for a senior

Designs exception-translation boundaries, never drops traces, uses suppressed exceptions correctly, and reasons about double-logging anti-patterns.

for a principal

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

context

open as a page

What is 'more-precise rethrow' analysis introduced in Java 7, and how does it let a method declare narrower checked exceptions than the catch type suggests?

level: middleimportance: should knowfreq 48%

basics

~20 s

Since Java 7, if you catch a broad type like Exception but only ever throw a couple of specific checked exceptions inside the try, the compiler is smart enough to know that. So throws can list just those specific types, not the broad caught type.

open as a page

When would you use a broad catch with more-precise rethrow versus a multi-catch block, and what are the trade-offs?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Use multi-catch when you want to name exactly the few exception types you handle. Use a broad catch (with precise rethrow) when you want one block to log or clean up for any exception but still rethrow with a precise contract. Multi-catch is more explicit; broad catch is more convenient.

open as a page

How can a method rethrow a checked exception without declaring it in its throws clause, and why is the 'sneaky throws' technique controversial?

level: principalimportance: nice to knowfreq 18%

basics

~20 s

You can trick the compiler with an unchecked generic cast so a checked exception is thrown without being declared — this is 'sneaky throws.' It works because the checked/unchecked rule is only enforced at compile time, not by the JVM. It's controversial because callers can't see or catch the exception by type.

open as a page