What are the common mistakes when wrapping exceptions, and how do you wrap correctly to preserve the original context?
answer
- never drop the cause — always pass e
- log once OR rethrow, not both
- don't copy only getMessage()
- add context (op + ids) to the message
- try-with-resources -> suppressed, not lost
basics
~20 sThe big mistakes are catching an exception and throwing a new one without passing the original as the cause, and logging plus rethrowing the same error (double logging). Always pass the caught exception as the cause and log it only once, at the boundary that handles it.
solid answer
~50 sThe cardinal sin is 'losing the cause': catch e, throw a new exception, but forget to pass e — now the root cause and its stack trace vanish, and you only see a vague top-level message. Always chain: new HigherLevelException("context", e). Related anti-patterns: catching Exception and rethrowing a generic RuntimeException with no message; 'log and rethrow' (logging the same exception at every layer produces duplicate noisy traces — log once where it's finally handled); swallowing the cause by only copying getMessage() into a new exception; and over-wrapping (re-wrapping an already-meaningful exception layer after layer). Correct wrapping translates the exception into the current layer's abstraction, adds context to the message (ids, parameters), preserves the original as the cause, and only converts checked-to-unchecked when that genuinely fits the API. For try-with-resources cleanup failures, rely on suppressed exceptions rather than discarding them.
code
java · 10 lines// WRONG: cause lost + double logging
catch (SQLException e) {
log.error("load failed", e); // logs...
throw new DataAccessException("load failed"); // ...drops e and logs again upstream
}
// RIGHT: translate, add context, chain, let the top layer log once
catch (SQLException e) {
throw new DataAccessException("load failed for user " + id, e);
}go deeper
Knows to pass the original exception as the cause and not throw it away.
Avoids log-and-rethrow and getMessage()-only wrapping; adds context to messages.
Applies the full wrapping recipe — narrow catch, translate to layer abstraction, chain, log once at the boundary, avoid over-wrapping, use try-with-resources for cleanup.
Codifies these as team conventions (exception-translation boundaries, single-logging policy, custom exception design) and bakes them into reviews/lint so the codebase produces clean, single, fully-chained traces.
## Why this matters Exception **wrapping** (or *translation*) is when you catch a low-level exception and throw a higher-level one that fits your layer's abstraction. Done right, it preserves diagnostics; done wrong, it destroys them. Here are the classic mistakes and the correct technique. ## Anti-pattern 1: Losing the cause ```java catch (SQLException e) { throw new DataAccessException("load failed"); // e is dropped! } ``` The original `SQLException` — message and stack trace — is gone. In production you see only "load failed" with no `Caused by:`. **Fix:** always pass the cause: ```java catch (SQLException e) { throw new DataAccessException("load failed for user " + id, e); } ``` ## Anti-pattern 2: Log-and-rethrow (double logging) ```java catch (IOException e) { log.error("read failed", e); // logged here... throw new ServiceException("read failed", e); // ...and again up the stack } ``` If every layer logs *and* rethrows, one failure produces several near-identical stack traces in the logs, making it look like multiple errors and burying the signal. **Rule:** *either* handle (and log) the exception, *or* propagate it — not both. Log **once**, at the boundary that actually deals with it (e.g. a top-level handler / controller advice). ## Anti-pattern 3: Swallowing the cause via getMessage() ```java catch (SQLException e) { throw new DataAccessException(e.getMessage()); // only the string survives } ``` Copying just the message keeps a hint but throws away the **stack trace** and the real exception object — there's still no `Caused by:`. Pass the Throwable, not its message string. ## Anti-pattern 4: Generic, context-free wrapping ```java catch (Exception e) { throw new RuntimeException(e); // no message, catches too much } ``` Catching `Exception` (or `Throwable`) is overly broad and the empty message adds no value. **Add context** — the operation, the ids/parameters — so the wrapper *earns its place*: `throw new RuntimeException("failed to index document " + docId, e);`. ## Anti-pattern 5: Over-wrapping Re-wrapping an already-meaningful exception at every layer creates a deep chain of redundant wrappers (`A caused by B caused by C…` all saying the same thing). Wrap only when you're genuinely **translating to a new abstraction**; otherwise let it propagate. ## Anti-pattern 6: Discarding cleanup failures In manual `try/finally`, if `close()` throws in the `finally`, it can **mask** the original exception. **Use try-with-resources**, which records the cleanup failure as a *suppressed* exception (via `addSuppressed`, read with `getSuppressed`) so both the primary failure and the cleanup failure survive. ## The correct wrapping recipe 1. **Catch the narrowest exception** you actually intend to translate. 2. **Add context** to the message (operation + key parameters). 3. **Pass the original as the cause** (constructor or initCause). 4. **Choose the right target type** — translate to your layer's abstraction; convert checked→unchecked only when it genuinely suits the API contract. 5. **Log once**, at the layer that finally handles it — pass the Throwable, not getMessage(). 6. **Don't over-wrap**; propagate when no translation is needed. 7. **Let try-with-resources** handle cleanup so failures become suppressed, not lost. Following this, your logs show a clean, single, fully-chained trace from symptom to root cause.
- Why is 'log and rethrow' at every layer harmful?It produces multiple near-duplicate stack traces for a single failure, inflating log noise and making one error look like many. Log once, at the boundary that actually handles the exception; otherwise just propagate.
- How do try-with-resources and suppressed exceptions relate to chaining mistakes?In manual try/finally, a close() that throws can mask the original exception. Try-with-resources instead records the cleanup failure as a suppressed exception (getSuppressed) so both the real failure and the cleanup error are preserved — the cause chain stays the primary causal link.
- When should you NOT wrap and instead just propagate?When the existing exception already fits the current layer's abstraction and adds the right context. Re-wrapping it only creates redundant 'caused by' layers that say the same thing. Wrap only when translating to a genuinely different abstraction.
saying these in an interview costs you the question
- Catching and rethrowing a new exception without passing the cause.
- Logging the same exception at every layer (double/triple logging).
- Wrapping with only getMessage() instead of the Throwable.
- Catching Exception/Throwable broadly and wrapping in a context-free RuntimeException.
- Letting a finally-block close() mask the original exception instead of using try-with-resources.