skip to content

When should you design an API to throw a checked exception versus an unchecked one?

level: seniorimportance: should knowfreq 50%

answer

  1. Checked = recoverable + you want to force handling
  2. Unchecked = programming error / precondition violation
  3. Test: can a real caller do something different?
  4. Translate low-level exceptions at boundaries, keep the cause
  5. Most modern code leans unchecked (boilerplate, leaks, lambdas)

basics

~20 s

Use a checked exception when the caller can reasonably recover and you want to force them to handle it. Use an unchecked exception for programming errors (bad arguments, illegal state) and conditions the caller usually can't fix.

solid answer

~50 s

The classic guideline (Effective Java): use checked exceptions for recoverable conditions the caller should be forced to address, and runtime (unchecked) exceptions for programming errors — violations of the method's preconditions, like an illegal argument or illegal state. The key question is: 'Can a reasonable caller recover from this, and do I want to compel them to acknowledge it?' If yes, checked; if it represents a bug or the caller realistically can't act on it, unchecked. In practice many teams lean heavily unchecked because checked exceptions create boilerplate, leak through abstraction layers, and clash with lambdas and streams. A common compromise: throw unchecked domain exceptions, document them with @throws, and reserve checked exceptions for a small set of genuinely recoverable, expected outcomes. Whatever you choose, when wrapping a lower-level exception keep the original as the cause to preserve the stack trace.

go deeper

for a junior

Knows checked is for recoverable problems and unchecked for bugs, with a basic example.

for a middle

Applies the 'can the caller recover?' test and uses IllegalArgumentException/IllegalStateException for precondition violations.

for a senior

Designs API contracts deliberately, translates exceptions at layer boundaries preserving the cause, and articulates the tradeoffs that push toward unchecked.

for a principal

Sets a team/codebase exception policy, weighs versioning and abstraction-leak implications, and can justify choices like Spring's unchecked DataAccessException or Kotlin dropping checked exceptions.

## The core decision When you design a method, you control which exceptions it throws and therefore which **contract** you impose on callers. Choosing checked vs unchecked is an API-design decision, not an implementation detail, because it changes what every caller must write. ## The textbook rule (Effective Java, Bloch) Three-way guidance: 1. **Checked exception** — for **recoverable conditions** where you want to *force* the caller to take action. Example: `InsufficientFundsException` on a withdrawal — the caller can prompt the user, retry with a smaller amount, etc. 2. **Unchecked (RuntimeException)** — for **programming errors**: the caller violated the method's documented preconditions. Examples: passing `null` where forbidden (`NullPointerException`), an out-of-range index (`IndexOutOfBoundsException`), calling a method in the wrong state (`IllegalStateException`). The fix is to correct the code, not to handle the exception. 3. **Error** — reserve for the JVM; don't subclass it for your own use. The decisive test: **'Can a reasonable caller recover, and should I compel them to deal with it?'** If both yes → checked. Otherwise → unchecked. ## Why 'recoverable' is subtle A condition is only meaningfully checked if there is a *distinct recovery action*. If every caller will just log and rethrow (or wrap), forcing catch-or-declare adds noise without value — making it unchecked is better. So the test is not 'could anyone theoretically recover' but 'will real callers do something different than they would for any other failure'. ## The case against checked exceptions (why teams go unchecked) - **Boilerplate**: `throws` lists propagate up many layers. - **Leaky abstraction**: a `SQLException` from the persistence layer should not appear in a service interface; if you declare `throws SQLException` you've leaked the implementation. Spring's `DataAccessException` hierarchy is unchecked precisely to avoid this. - **Swallowing**: under pressure, developers write empty catch blocks, hiding failures. - **Lambda/stream friction**: functional interfaces (`Function`, `Supplier`) don't declare checked exceptions, so you can't throw a checked exception directly from a stream pipeline without wrapping. - **Versioning rigidity**: adding a new checked exception to a method breaks all callers' source. Because of these, Kotlin removed checked exceptions entirely, and many influential Java libraries (Spring, Hibernate) expose only unchecked exceptions. ## A pragmatic policy 1. Default to **unchecked** domain exceptions for most failures, documented with `@throws` Javadoc. 2. Use **checked** only for a small, deliberate set of recoverable, expected outcomes where compelling the caller is genuinely valuable. 3. **Translate at boundaries**: catch low-level exceptions and rethrow a domain exception that fits your abstraction layer — but always pass the original as the **cause** (`throw new OrderException("...", e)`) so the root cause and full stack trace survive. 4. **Never swallow**: an empty catch block is almost always a bug; at minimum log with context or rethrow. 5. Validate preconditions eagerly and throw `IllegalArgumentException` / `IllegalStateException` / `NullPointerException` (via `Objects.requireNonNull`) for caller bugs. ## Worked example ```java // Recoverable, force the caller to deal with it -> checked public void withdraw(Money amount) throws InsufficientFundsException { ... } // Caller bug -> unchecked, document it /** @throws IllegalArgumentException if amount is negative */ public void deposit(Money amount) { if (amount.isNegative()) throw new IllegalArgumentException("amount must be >= 0"); } ```

  • Why does Spring's DataAccessException hierarchy use unchecked exceptions?
    To stop low-level, vendor-specific SQLExceptions from leaking through service interfaces and forcing throws clauses everywhere. Unchecked lets callers handle DB errors only where they actually can, and keeps the persistence abstraction clean.
  • What's the danger of catching an exception just to log and rethrow as a different type?
    If you don't pass the original as the cause, you lose the root stack trace, making debugging much harder. Always use the constructor that takes a cause (or initCause).

saying these in an interview costs you the question

  • 'Always use checked exceptions to be safe' (creates boilerplate and swallowing)
  • Declaring throws SQLException on a service interface (leaks the layer)
  • Wrapping without preserving the original cause
  • Using exceptions for normal control flow
  • Treating every possible failure as recoverable and therefore checked

context