skip to content

How do you re-throw a caught exception, and what is the difference between re-throwing as-is and wrapping it in a new exception?

level: seniorimportance: should knowfreq 50%

answer

  1. throw e; re-throws unchanged, same stack trace
  2. new X(msg, cause) wraps + chains via getCause()
  3. wrapping translates low-level to domain exceptions
  4. never drop the cause (the cardinal sin)
  5. Java 7: precise re-throw narrows broad catch params
  6. addSuppressed for secondary cleanup failures

basics

~20 s

Inside a catch block you can throw the same exception again to let it keep propagating, or you can throw a new exception that includes the original as its 'cause'. Wrapping lets you raise a more meaningful exception while preserving the original stack trace.

solid answer

~50 s

Inside a catch block, `throw e;` re-throws the caught exception unchanged so it continues up the stack — useful when you only wanted to log or partially handle it. Wrapping means constructing a new, more meaningful exception and passing the original as the cause: `throw new ServiceException("load failed", e)`. The cause chain (retrieved via getCause) preserves the original stack trace so nothing is lost. Wrapping is how you translate low-level exceptions (e.g. SQLException) into your domain's abstraction without leaking implementation details, and it's also how checked exceptions get converted to unchecked ones. The key rule: never swallow the original — always pass it as the cause (or use Throwable.addSuppressed for secondary failures). Since Java 7, 'precise re-throw' lets the compiler narrow what a re-thrown variable can be, so you can declare the specific types in throws even when caught as a broader type.

go deeper

for a junior

Knows you can throw e again inside a catch to keep it propagating.

for a middle

Distinguishes re-throwing as-is from wrapping and knows to pass the cause when wrapping.

for a senior

Explains chaining, abstraction-boundary translation, checked↔unchecked conversion, suppressed exceptions, and avoids swallowing causes.

for a principal

Defines codebase-wide exception-translation policy and layering conventions, and reasons about precise re-throw and diagnostic preservation at scale.

## The scenario You caught an exception. Now what? You have three honest options: handle it fully (and don't re-throw), **re-throw it as-is**, or **wrap it** in a different exception and throw that. This question is about the last two. ## Re-throwing as-is ```java try { risky(); } catch (IOException e) { log.warn("risky failed, propagating", e); throw e; // same object continues up the stack } ``` Here you catch the exception, do something (log, increment a metric, clean up), then `throw e;` to let it keep propagating to a higher-level handler. The exception object and its original stack trace are unchanged. ## Wrapping (exception chaining) ```java try { jdbc.query(...); } catch (SQLException e) { throw new RepositoryException("failed to load user " + id, e); } ``` You construct a **new** exception of a type that makes sense at *this* layer and pass the caught one as the **cause** (the second constructor argument, or via `initCause`). This is **exception chaining**. ### What "cause" means Every `Throwable` can hold a reference to another `Throwable` that caused it, accessible with `getCause()`. When you print the stack trace, Java shows the wrapper *and* a `Caused by:` section with the original's trace. So **wrapping loses nothing** — the root cause and its location are preserved, while the message becomes meaningful at the current abstraction level. ### Why wrap? 1. **Abstraction / encapsulation** — callers of a repository shouldn't need to know it uses JDBC; converting `SQLException` to a domain `RepositoryException` hides that detail. 2. **Checked → unchecked** — wrap a checked exception in a `RuntimeException` to avoid forcing every caller to declare it. 3. **Adding context** — the wrapper message can include the inputs (`user " + id`) that the low-level exception lacked. ## The cardinal sin: swallowing the cause ```java catch (SQLException e) { throw new RepositoryException("failed"); // BUG: cause is dropped! } ``` Forgetting to pass `e` discards the original stack trace, making the bug nearly undebuggable. Always pass the cause. ## Suppressed exceptions (a related tool) When a *secondary* failure happens during cleanup (e.g. a `close()` failing while another exception is already in flight), use `primary.addSuppressed(secondary)` rather than letting the secondary mask the primary. try-with-resources does this automatically. ## Java 7 precise re-throw Before Java 7, catching `Exception` and re-throwing forced you to declare `throws Exception`. Since Java 7, if the caught variable is **effectively final** and the try body can only throw specific subtypes, the compiler tracks those precise types — so you may declare just those in the method's `throws` clause even though the catch parameter is broader: ```java void m() throws IOException, ParseException { try { ... } catch (Exception e) { // catches broadly log(e); throw e; // compiler knows e is only IOException or ParseException here } } ``` ## Defining the terms - **Cause / chaining**: a `Throwable` referencing the underlying `Throwable` that triggered it, via `getCause()`/the cause constructor. - **Suppressed exception**: a secondary exception attached to a primary one via `addSuppressed`, so it isn't lost. - **Precise re-throw**: the Java 7 compiler analysis that narrows a re-thrown broad catch parameter to its actual possible types. ## Rule of thumb Re-throw as-is when the exception is already meaningful at higher layers and you only needed a side effect (log/cleanup). Wrap when you're crossing an abstraction boundary or converting checked↔unchecked — and **always** preserve the cause.

  • Does re-throwing with throw e reset the stack trace?
    No. The original stack trace is preserved. (Calling fillInStackTrace() would overwrite it, but a plain throw e does not.)
  • How do you preserve the original exception when wrapping?
    Pass it as the cause: throw new MyException("msg", originalException). It becomes retrievable via getCause() and shows as 'Caused by:' in the stack trace.
  • What is a suppressed exception?
    A secondary exception that occurred while another was propagating (e.g., a close() failure). It is attached via addSuppressed so it isn't lost; try-with-resources does this automatically.

saying these in an interview costs you the question

  • Wrapping but forgetting to pass the original as the cause
  • Believing re-throwing resets/erases the stack trace
  • Catching and swallowing (empty catch) instead of re-throwing or handling
  • Thinking you must always wrap rather than sometimes re-throw as-is
  • Leaking low-level exception types across an abstraction boundary

context