skip to content

Exception Translation

Translating a low-level exception into one that fits your abstraction — while chaining the original as the cause — stops implementation details leaking to callers. Interviewers ask about it when discussing layered architectures and repository interfaces.

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

questions

5

You see this code in a review. What is wrong with it and how would you fix it?

level: juniorimportance: must knowfreq 60%

answer

  1. No 'Caused by:' = cause was dropped
  2. Pass e as the second constructor arg
  3. Add super(message, cause) constructor
  4. initCause(e) if no cause constructor
  5. Translating type good; dropping cause bad

basics

~10 s

It throws a new exception but never passes the original (e) as the cause, so the real error and its stack trace are lost. Fix it by chaining: throw new ServiceException("...", e).

solid answer

~50 s

The bug is that the catch block translates the exception to a higher-level type but doesn't chain the original — it constructs ServiceException with only a message, dropping e. This is exception masking: when the failure surfaces, you'll see 'ServiceException: could not load user' with no 'Caused by:' section, so you lose the actual reason (e.g. a connection timeout) and the original stack trace, making the bug far harder to diagnose. The fix is to pass e as the cause: throw new ServiceException("could not load user " + id, e). That preserves the full chain in the stack trace while still exposing a layer-appropriate type to callers. If ServiceException lacks a (String, Throwable) constructor, add one that calls super(message, cause), or use initCause(e). Translating the type is fine and good; the only defect is dropping the cause.

code

java · 16 lines
java
// BEFORE (bug): cause dropped -> no 'Caused by:' in trace
catch (DataAccessException e) {
    throw new ServiceException("could not load user " + id);
}

// AFTER (fixed): cause chained -> full diagnostic chain preserved
catch (DataAccessException e) {
    throw new ServiceException("could not load user " + id, e);
}

// Requires a cause-accepting constructor:
public class ServiceException extends RuntimeException {
    public ServiceException(String message, Throwable cause) {
        super(message, cause);
    }
}

go deeper

for a junior

Spots that e isn't passed and fixes it by chaining; knows the (String, Throwable) constructor pattern.

for a middle

Explains why the missing cause matters for debugging and that translating the type itself is correct.

for a senior

Notes that logging-only is insufficient, discusses initCause vs constructor, and ties it to team lint rules forbidding masking.

for a principal

Connects to org-wide error-handling standards, static-analysis enforcement, and how lost causes degrade incident response/observability.

## The code under review ```java public User loadUser(long id) { try { return repository.findById(id); } catch (DataAccessException e) { throw new ServiceException("could not load user " + id); // BUG: e is not passed } } ``` ## What is happening here (terms defined) An **exception** is an object signalling a failure that propagates up the call stack. A **stack trace** is the recorded list of method calls showing *where* it happened. The **cause** of an exception is another exception that triggered it; Java prints it as a `Caused by:` block in the trace, forming a *chain* back to the original error. This method does **exception translation** — it catches a lower-level `DataAccessException` and throws a higher-level `ServiceException` appropriate to the service layer. Translation itself is *good practice*: it stops the data layer's type from leaking to callers. ## The defect: the cause is dropped (masking) The `ServiceException` is created with only a message string. The caught exception `e` is **never attached**. This is **exception masking** (or swallowing the cause). Consequences: - The printed trace shows only `ServiceException: could not load user 42` and the stack from *this* method onward. - There is **no `Caused by:`** — the real reason (maybe a SQL timeout, a constraint violation, a closed connection) and its stack trace are gone. - Whoever debugs the production incident has lost the single most useful piece of evidence. ## The fix: chain the cause Pass the original exception as the **cause** so the chain is preserved: ```java public User loadUser(long id) { try { return repository.findById(id); } catch (DataAccessException e) { throw new ServiceException("could not load user " + id, e); // e chained as cause } } ``` For this to compile, `ServiceException` must have a constructor that accepts a cause: ```java public class ServiceException extends RuntimeException { public ServiceException(String message, Throwable cause) { super(message, cause); // passes the cause up to Throwable } } ``` If you can't add such a constructor, use `Throwable.initCause`: ```java ServiceException ex = new ServiceException("could not load user " + id); ex.initCause(e); throw ex; ``` Now the trace shows the `ServiceException` *and* `Caused by: ...DataAccessException...` with the original stack — full diagnostics retained, while callers still only see the clean `ServiceException` type. ## What is NOT wrong - **Translating the type is correct** — exposing `ServiceException` instead of `DataAccessException` keeps the data layer from leaking. - **Catching `DataAccessException` specifically** (rather than `Exception`) is fine and even good. The entire bug is the missing cause. The lesson: *translate the type, but always chain the original.*

  • How do you preserve the cause if ServiceException has no (String, Throwable) constructor?
    Either add one that calls super(message, cause), or after constructing the exception call ex.initCause(e) before throwing it.
  • Is just logging e and then throwing the new exception without the cause acceptable?
    No — logging helps locally but any upstream handler that catches ServiceException still sees no cause. Chain the cause so the information travels with the exception.

saying these in an interview costs you the question

  • Saying the whole catch should be removed (translation is fine)
  • Logging e but still throwing without it (partial fix; still masks for upstream handlers)
  • Catching Exception instead of the specific type as the 'fix'
  • Thinking the message string alone preserves the original error

context

open as a page

What is exception translation in Java, and why would you do it instead of just letting a low-level exception propagate?

level: middleimportance: must knowfreq 72%

basics

~10 s

Exception translation means catching a low-level exception and throwing a new, higher-level one that fits the layer you're in. You do it so callers see errors that match your abstraction, not your internal plumbing.

open as a page

What is the difference between exception translation, exception chaining, and exception masking (swallowing)?

level: middleimportance: should knowfreq 55%

basics

~20 s

Translation = throw a new, higher-level exception. Chaining = attach the original as the cause of that new exception. Masking = throw or catch in a way that loses the original. You usually translate AND chain, and never mask.

open as a page

When translating an exception at a layer boundary, how do you decide between throwing a checked or an unchecked exception, and what should its type be?

level: seniorimportance: should knowfreq 48%

basics

~20 s

Throw a checked exception only if the caller can realistically recover and you want to force them to handle it. For programming errors or unrecoverable failures, throw an unchecked (RuntimeException) one. Pick a type whose name and meaning fit the caller's layer.

open as a page

When should you NOT translate an exception, and what are the costs of over-translating across many layers?

level: principalimportance: nice to knowfreq 33%

basics

~10 s

Don't translate when the existing exception is already meaningful to the caller, or when wrapping adds nothing. Over-translating creates deep 'Caused by' chains, loses specificity, and adds maintenance noise.

open as a page