What is exception translation in Java, and why would you do it instead of just letting a low-level exception propagate?
answer
- Catch low-level, throw layer-appropriate
- Don't leak SQLException/JDBC upward
- Always chain the original as cause
- Stable API when impl changes
- Translate at boundaries, not everywhere
basics
~10 sException 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.
solid answer
~50 sException translation is catching a low-level exception thrown by a lower layer and rethrowing a higher-level exception that is meaningful at your abstraction. For example, a repository catches a SQLException and throws a DataAccessException. You do this so the exceptions a method declares stay aligned with what it promises: a 'UserRepository.findById' should fail with a data-access error, not leak that JDBC or a specific driver is underneath. This keeps the API contract stable when the implementation changes (swap JDBC for an ORM and callers don't break) and avoids forcing callers to depend on, import, or handle low-level types. The key discipline is to chain the original exception as the cause so you keep the full stack trace and don't lose diagnostic information. Translate only where it adds meaning; needless wrapping at every layer is noise.
go deeper
Can define it: catch a low-level exception and throw a more meaningful one, keeping the original as the cause.
Explains the why (stable contract, no leaking details) and reliably chains the cause; gives a repository/SQLException example.
Discusses where to translate (layer boundaries only), the trade-off of over-wrapping, checked vs unchecked choices, and cites Spring's DataAccessException as a model.
Frames it as part of error-contract design across modules — how the exception hierarchy is shaped, how it interacts with logging/observability and API versioning, and when NOT to translate.
## The problem exception translation solves In Java, an **exception** is an object thrown to signal that something went wrong; it travels up the call stack until some `catch` block handles it. A **checked exception** must be declared in a method's `throws` clause and the caller is forced to handle or re-declare it; an **unchecked exception** (a subclass of `RuntimeException`) does not have to be declared. Real systems are built in **layers** (also called *abstraction levels*). A typical stack: a web controller calls a service, the service calls a repository (data-access layer), and the repository talks to a database via JDBC. Each layer has its own vocabulary — the controller thinks in HTTP, the repository thinks in 'rows and entities', JDBC thinks in `SQLException`. If a low-level exception like `java.sql.SQLException` is allowed to fly all the way up untouched, every caller above the repository is now coupled to JDBC. Their code must import `SQLException`, their `catch` blocks reference it, and their method signatures may have to declare it. This is called a **leaky abstraction**: implementation details (here, 'we use SQL') escape through the API and force the rest of the system to know about them. ## What exception translation is **Exception translation** is the practice of *catching* a lower-level exception and *throwing a new, higher-level exception that is appropriate to the abstraction the current layer presents*. The repository catches `SQLException` and throws, say, `DataAccessException` (a higher-level type the rest of the app understands). The caller now only ever sees `DataAccessException`, which means 'something went wrong reading/writing data' — without knowing or caring that SQL was involved. ```java public User findById(long id) { try { return jdbc.queryForUser(id); } catch (SQLException e) { throw new DataAccessException("Failed to load user " + id, e); // higher-level, e chained as cause } } ``` The phrase 'appropriate to the abstraction' is the heart of it. The thrown type should make sense in the *caller's* world. A `UserService` that fails to find a user might translate further to a domain-level `UserNotFoundException`. ## Why do it (the benefits) 1. **Stable API contract.** The exceptions a method can throw are part of its public contract. If you swap the implementation (JDBC → JPA/Hibernate, or a SQL DB → a NoSQL store), the low-level exception types change. If callers caught those, they'd all break. Translating to a stable higher-level type insulates them. 2. **No leaking of implementation details.** Callers don't learn that you use SQL, a particular driver, a file, or a remote API. This keeps modules decoupled. 3. **Meaningful errors.** A `DataAccessException` or `OrderProcessingException` tells the caller something actionable at their level; a raw `SocketTimeoutException` does not. 4. **Cleaner caller code.** Callers don't have to import or handle a zoo of low-level types from layers they shouldn't know about. ## The non-negotiable rule: chain the cause When you throw the new exception, you must pass the original as the **cause**. In Java this is done via the constructor `new HighLevelException(message, cause)` or `initCause(cause)`. The cause is preserved and printed in the stack trace as a 'Caused by:' section, so you keep *all* the diagnostic detail (the real SQL error, the line it failed on). Throwing a new exception *without* the cause is **exception masking** — you destroy the evidence and make debugging far harder. This is the single most common mistake. ## How it differs from blind propagation - **Blind propagation** = let the original exception keep flying (or re-throw it as-is). Cheap, but leaks the low-level type upward. - **Translation** = deliberately convert to a layer-appropriate type *and chain the original*. Costs a little code but keeps abstractions clean. Translation is not free: each wrap adds a layer to the stack trace and a class to maintain. So **translate only where it adds meaning** — typically at module/layer boundaries — not mechanically at every method. Over-wrapping produces deeply nested 'Caused by' chains that are noise. This idea is widely cited (Effective Java, Item 'Throw exceptions appropriate to the abstraction'). Spring's `DataAccessException` hierarchy is a canonical production example: it translates vendor-specific `SQLException`s into a consistent, unchecked, technology-agnostic hierarchy.
- How do you preserve the original exception when translating?Pass it as the cause — via the constructor new HighLevelException(message, cause) or initCause(). It then appears as 'Caused by:' in the stack trace, preserving the full diagnostic chain.
- Give a real-world library that does exception translation.Spring's DataAccessException hierarchy translates vendor-specific java.sql.SQLExceptions into a consistent, unchecked, technology-agnostic set of exceptions so callers aren't coupled to JDBC.
saying these in an interview costs you the question
- Throwing the new exception without chaining the cause (masking)
- Translating at every single method, creating deep noisy cause chains
- Thinking translation means just renaming the message while keeping the same type
- Believing it's only for checked exceptions