skip to content

When you catch an exception you cannot fully handle, what are your options, and how do you preserve the original cause?

level: middleimportance: should knowfreq 55%

answer

  1. Rethrow OR wrap-and-rethrow
  2. Wrap = translate to your layer's type
  3. Always pass the cause (msg, e)
  4. 'Caused by:' is the dropped link if you forget e
  5. Don't wrap when it adds nothing

basics

~20 s

You can rethrow the same exception, or wrap it in a new, more meaningful exception. When you wrap, always pass the original as the 'cause' so the full chain and stack trace are kept and you can still see where it really started.

solid answer

~50 s

If a catch site cannot fully recover, it should not swallow — it should propagate. Two patterns: rethrow the original exception unchanged, or wrap it in a more meaningful one for the current abstraction layer (e.g. translate a low-level `SQLException` into a domain `RepositoryException`). The critical rule when wrapping is to preserve the cause: pass the original exception to the new exception's constructor (`new RepositoryException(msg, e)`) or call `initCause`. This builds an **exception chain** so the stack trace shows both the high-level failure and the original root, printed as 'Caused by:'. Dropping the cause — `throw new RepositoryException(msg)` without `e` — destroys that link and is nearly as bad as swallowing. Wrapping is how you hide implementation details from callers while keeping full diagnosability. Avoid wrapping when it adds no information; sometimes a clean rethrow is best.

code

java · 15 lines
java
// Exception translation that preserves the root cause
class UserRepository {
    User load(long id) {
        try {
            return jdbc.queryForObject(SQL, mapper, id);
        } catch (SQLException e) {
            // wrap to the domain layer, but keep the cause (the second arg)
            throw new RepositoryException("failed to load user " + id, e);
        }
    }
}

// BAD: cause dropped -> 'Caused by:' is gone, root cause lost
// throw new RepositoryException("failed to load user " + id);
// throw new RuntimeException(e.getMessage()); // also loses type + stack trace

go deeper

for a junior

Knows you can rethrow an exception and that wrapping should keep the original somehow.

for a middle

Wraps with the cause argument, explains the 'Caused by:' chain, and avoids dropping the cause or losing the stack trace via getMessage().

for a senior

Applies exception translation deliberately per layer, decides checked-vs-unchecked propagation policy, and avoids duplicate log-and-rethrow.

for a principal

Sets cross-service error-translation conventions (which boundary maps which exceptions), standardizes domain exception types, and ensures causes survive across async/remote boundaries.

## The situation You are in a `catch` block. The exception is real, but **this layer cannot meaningfully fix it** — a data-access class catches a `SQLException`, but it can't decide whether to retry, show an error page, or abort; that's the caller's job. Swallowing is wrong (the error must not vanish). So you **propagate**: send the failure onward. ## Option 1 — rethrow unchanged The simplest propagation is to re-throw what you caught: ```java catch (IOException e) { metrics.increment("io.failure"); // do a side task, but don't handle throw e; // let the caller deal with it } ``` Use this when you only wanted to observe the failure (log it, record a metric) but the *handling* belongs higher up. The original stack trace is untouched. ## Option 2 — wrap and rethrow (exception translation) Often you want to **translate** a low-level exception into one that fits your layer's abstraction, so callers don't depend on implementation details. A repository shouldn't make every caller know about JDBC's `SQLException`; it throws a domain `RepositoryException` instead: ```java catch (SQLException e) { throw new RepositoryException("failed to load user " + id, e); // note the 'e' } ``` The **second argument** `e` is the **cause**. It is the whole point. ## What 'cause' and 'chaining' mean Every `Throwable` can hold a reference to another `Throwable` that caused it — its **cause**. You set it via a constructor that takes `(String message, Throwable cause)` or via `initCause(Throwable)`. This forms a linked chain: top-level exception → its cause → that cause's cause, and so on. When the chain is printed, you see: ``` RepositoryException: failed to load user 42 at UserRepo.load(UserRepo.java:31) ... Caused by: java.sql.SQLException: connection reset at ... ``` The `Caused by:` section is the original problem. Without it, you'd see only 'failed to load user 42' and have no idea the real issue was a dropped DB connection. ## The cardinal mistake: dropping the cause ```java catch (SQLException e) { throw new RepositoryException("failed to load user " + id); // BUG: no cause! } ``` This is almost as harmful as an empty catch. The new exception propagates, but the original `SQLException`'s message and stack trace are gone — you lose the root cause. A common variant is `throw new RuntimeException(e.getMessage())`, which keeps only the text and loses the type and trace. Always pass the exception object itself. ## When NOT to wrap Wrapping adds a layer; do it only when it adds value (a clearer message or a layer-appropriate type). If the caught exception is already meaningful to the caller, a plain rethrow is cleaner. Avoid wrapping an exception in the *same* type for no reason, and avoid double-wrapping that produces deep, noisy chains. ## Checked vs. unchecked while propagating If you rethrow a checked exception, your method must declare it with `throws` (or wrap it in an unchecked one to cross an API where you don't want a checked signature). Wrapping a checked `SQLException` into an unchecked `RepositoryException extends RuntimeException` is a common way to remove checked-exception noise from a clean domain API — but only if your team has decided that policy.

  • What is the difference between log-and-rethrow and log-and-swallow?
    Both log, but rethrow lets the failure continue to a handler that can act; swallow stops it. Beware log-AND-rethrow at multiple layers: it produces duplicate log entries for one failure — log once, near the boundary, and just rethrow elsewhere.
  • How do you preserve the cause when the constructor has no (String, Throwable) overload?
    Construct the exception, then call initCause(original) before throwing it. Every Throwable supports initCause exactly once.

saying these in an interview costs you the question

  • throw new XException(msg) — dropping the original cause
  • throw new RuntimeException(e.getMessage()) — losing type and stack trace
  • Wrapping for no reason / double-wrapping into deep noisy chains
  • Rethrowing a checked exception without declaring it (won't compile)

context