skip to content

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%

answer

  1. Checked = recoverable + force handling
  2. Unchecked = unrecoverable / avoid signature pollution
  3. Modern/Spring leans unchecked (DataAccessException)
  4. Type must fit caller's layer + be granular
  5. Provide subtypes; never bare RuntimeException

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.

solid answer

~50 s

The decision hinges on recoverability and caller burden. Use a checked exception when the caller can reasonably recover and you want the compiler to force them to deal with it — but checked exceptions don't compose well across many layers and pollute signatures, so most modern code and frameworks favor unchecked. Use an unchecked (RuntimeException-derived) exception for conditions the caller usually can't recover from, or to avoid forcing every layer to declare a checked type. Spring deliberately translates JDBC's checked SQLException into an unchecked DataAccessException hierarchy for this reason. As for the type itself: it should be named and structured for the *caller's* abstraction — e.g. a domain UserNotFoundException, or a coarse DataAccessException with meaningful subtypes for distinguishable cases (duplicate key, optimistic-lock failure). Avoid translating to overly generic types like raw RuntimeException or Exception, which give the caller nothing to branch on. Always chain the original cause regardless of checked/unchecked.

go deeper

for a junior

Knows that some exceptions are checked (must handle) and some are not, and that you pick a meaningful type.

for a middle

Can apply the recoverability rule and gives a sensible domain type, always chaining the cause.

for a senior

Reasons about composition/signature pollution, why unchecked dominates modern code, and designs a granular type hierarchy (Spring DataAccessException model).

for a principal

Owns the cross-module error contract: defines the exception hierarchy, checked/unchecked policy, how it maps to API/HTTP errors and observability, and migration/versioning impact.

## Background: checked vs unchecked exceptions Java splits exceptions into two families: - **Checked exceptions** (subclasses of `Exception` but not `RuntimeException`): the compiler *forces* the caller to either catch them or declare them in `throws`. Intended for *recoverable* conditions the caller should be prepared for (e.g. `IOException`). - **Unchecked exceptions** (subclasses of `RuntimeException`): no compiler obligation; can propagate silently. Intended for *programming errors* (`NullPointerException`, `IllegalArgumentException`) or conditions the caller typically can't recover from. When you do **exception translation** (catch a low-level exception, throw a higher-level one appropriate to your layer), you must choose which family the *new* exception belongs to, and what its concrete type is. ## Choosing checked vs unchecked **Throw checked when:** - The caller can plausibly **recover** (retry, fall back, ask the user) AND - You want the **compiler to guarantee** they consider it. Example: a file-import API might throw a checked `ImportFormatException` so callers must decide what to do with a bad file. **Throw unchecked when:** - The condition usually **can't be recovered** at the call site (a misconfigured database, a corrupt invariant), OR - Forcing a checked type would make it **propagate through many layers**, polluting every signature with `throws`, OR - The failure represents a **programming/contract violation**. ### Why modern code leans unchecked Checked exceptions don't *compose*. If a low-level method throws a checked `SQLException`, every method up the stack must declare it or wrap it — leaking the abstraction or forcing boilerplate. They also interact badly with lambdas/streams, which can't throw checked exceptions from standard functional interfaces. This is why frameworks like **Spring** translate the checked `java.sql.SQLException` into an **unchecked** `DataAccessException` hierarchy: callers handle data errors only where they actually can, without being forced to declare them everywhere. *Effective Java* advises using checked exceptions only for conditions the caller can reasonably be expected to recover from. ## Choosing the concrete type The translated type must be **meaningful at the caller's abstraction** and **granular enough to act on**: 1. **Name it for the caller's world.** `UserNotFoundException`, `PaymentDeclinedException`, `DataAccessException` — not `MyDbError` or a leaked `SQLException`. 2. **Provide a hierarchy when callers need to distinguish cases.** Spring's `DataAccessException` has subtypes like `DuplicateKeyException`, `OptimisticLockingFailureException`, `DataIntegrityViolationException`. A caller can `catch (DuplicateKeyException)` to handle a uniqueness conflict specifically, or `catch (DataAccessException)` for the general case. Translating everything to one flat type forces brittle message-string parsing. 3. **Don't translate to bare `RuntimeException`/`Exception`.** They carry no semantics; the caller can't branch on them and may accidentally catch unrelated failures. 4. **Carry useful context** (the entity id, the operation) in the message or fields, and **always chain the cause** so the original is preserved. ## Putting it together ```java public User findById(long id) { try { return jdbc.queryForUser(id); } catch (EmptyResultDataAccessException e) { // domain-level, unchecked, specific throw new UserNotFoundException(id, e); } catch (DataAccessException e) { // already a good higher-level type; rethrow or wrap with context throw new UserLookupException("Failed loading user " + id, e); } } ``` ## Common trade-offs / judgement calls - **Don't over-translate.** Re-wrapping an already-appropriate higher-level exception just deepens the cause chain. If the lower layer already throws a clean abstraction (like Spring's `DataAccessException`), often you let it pass or only translate to a *domain* type where it adds meaning. - **Consistency across the codebase** matters more than any single choice — a team convention (e.g. 'data layer throws unchecked DataAccessException subtypes') beats ad-hoc per-method decisions. - **API stability:** the translated type is part of your contract; changing it later breaks callers, so design the hierarchy deliberately.

  • Why does Spring translate the checked SQLException into an unchecked DataAccessException?
    Because forcing callers to handle a checked SQLException at every layer pollutes signatures and leaks JDBC; an unchecked, technology-agnostic hierarchy with meaningful subtypes lets callers handle data errors only where they can recover.
  • What's the downside of translating everything into one generic exception type?
    Callers can't distinguish cases (e.g. duplicate key vs not found) without parsing message strings, which is brittle. A typed hierarchy lets them catch the specific subtype they can handle.

saying these in an interview costs you the question

  • Always wrapping in raw RuntimeException with no specific type
  • Making everything checked, polluting every signature with throws
  • Translating to a single flat type, forcing message-string parsing to distinguish cases
  • Forgetting to chain the cause

context