skip to content

When designing a custom Java exception, what is the difference between a checked and an unchecked exception, and how do you decide which one to extend?

level: juniorimportance: must knowfreq 80%

answer

  1. Checked = subclass of Exception (not RuntimeException) = compiler forces catch-or-declare
  2. Unchecked = subclass of RuntimeException = no enforcement
  3. Checked for recoverable conditions the caller should handle; unchecked for programming bugs
  4. Trade-off: API contract clarity vs boilerplate + leaky abstractions
  5. Modern trend (Spring, Kotlin) leans unchecked

basics

~20 s

A checked exception (extends Exception) must be declared or caught — the compiler forces it. An unchecked exception (extends RuntimeException) does not. Use checked for problems the caller can recover from, unchecked for programming bugs.

solid answer

~40 s

In Java, a custom exception's category is decided by what it extends. Extending Exception (but not RuntimeException) makes it checked: any method that may throw it must declare it with throws, and callers must either catch it or re-declare it, enforced at compile time. Extending RuntimeException makes it unchecked: no compiler enforcement. The classic design rule (from Effective Java) is to use checked exceptions for recoverable conditions the caller is reasonably expected to handle and act on, and unchecked exceptions for programming errors and conditions from which recovery is impossible or unlikely. So a 'file format invalid, ask the user for another file' situation suits a checked exception, while 'index out of bounds' or 'null argument' is a programming bug and should be unchecked.

code

java · 20 lines
java
// Checked: extends Exception -> caller is forced to handle or declare
class InvalidUploadException extends Exception {
    public InvalidUploadException(String message) { super(message); }
}

// Unchecked: extends RuntimeException -> no compiler ceremony (programming error)
class ConfigKeyMissingException extends RuntimeException {
    public ConfigKeyMissingException(String key) {
        super("Missing required config key: " + key);
    }
}

void parse(File f) throws InvalidUploadException { // must declare the checked one
    if (!isValid(f)) throw new InvalidUploadException("bad format");
}

String get(String key) { // no throws needed for the unchecked one
    if (!config.containsKey(key)) throw new ConfigKeyMissingException(key);
    return config.get(key);
}

go deeper

for a junior

Can state that checked extends Exception and the compiler forces you to catch or declare it, while unchecked extends RuntimeException and doesn't. Knows the basic rule: checked for recoverable, unchecked for bugs.

for a middle

Can correctly place RuntimeException under Exception in the hierarchy, explain catch-or-declare precisely, and give concrete examples of when each is appropriate when writing a custom exception.

for a senior

Articulates the trade-offs (contract clarity vs boilerplate and leaky abstractions), references the recoverable-vs-programming-error rule, and can justify a default-to-unchecked policy while naming legitimate uses of checked.

for a principal

Frames it as an API-design and team-policy decision, discusses framework strategies (Spring/Hibernate wrapping), stream/lambda interactions, the cross-language trend, and can set a consistent exception-design convention for a codebase.

## What an exception is An **exception** in Java is an object that represents an abnormal event interrupting normal program flow. When code hits a problem it `throw`s an exception object; the JVM then unwinds the call stack looking for a matching `catch` block. All exceptions descend from the class `Throwable`. ## The Throwable hierarchy - `Throwable` is the root. - `Error` — serious problems the application should NOT try to handle (e.g. `OutOfMemoryError`). Treated like unchecked. - `Exception` — the base for application-level problems. - `RuntimeException` — a special subclass of `Exception`. The single rule that decides 'checked vs unchecked' is purely about position in this tree: - **Checked** = a subclass of `Exception` that is NOT a subclass of `RuntimeException`. - **Unchecked** = a subclass of `RuntimeException` (or of `Error`). ## What 'checked' means mechanically The Java **compiler** enforces a rule for checked exceptions called the *catch-or-declare requirement* (also 'handle or declare'). If a method body can throw a checked exception, the method must either: 1. wrap the call in a `try/catch` that handles it, or 2. add `throws ThatException` to its own signature, pushing the obligation to its callers. If you do neither, the code does **not compile**. Unchecked exceptions have no such requirement — you may throw them anywhere with zero ceremony, and the signature need not mention them. ## How you make a custom exception checked or unchecked You choose by which class you extend: ```java class InvalidConfigException extends Exception {} // checked class MissingConfigKeyException extends RuntimeException {} // unchecked ``` Nothing else distinguishes them — there is no keyword or annotation. ## The design decision (the real subject of this leaf) The authoritative guidance, from Joshua Bloch's *Effective Java*: - Use a **checked** exception when the caller can **reasonably be expected to recover**. By making it checked you force the caller to confront the failure at compile time. Example: a payment gateway timeout where the caller might retry, or a parse error where the caller can ask for different input. - Use an **unchecked** (runtime) exception for **programming errors** — precondition violations the caller should simply have avoided. Examples: passing `null` where it is illegal (`NullPointerException`), an out-of-range index (`IndexOutOfBoundsException`), or calling a method in the wrong state (`IllegalStateException`). Recovery is not the point; fixing the bug is. - For conditions where recovery is **impossible or implausible**, prefer unchecked too — forcing a `catch` no one can act on just adds noise. ## Trade-offs **Checked pros:** the failure is part of the API contract; the compiler guarantees callers acknowledge it; failures cannot be silently ignored. **Checked cons:** boilerplate (`throws` clauses everywhere, ceremonial `try/catch`); they leak through abstraction layers — a low-level `SQLException` propagating up forces unrelated upper layers to know about it; they compose badly with lambdas/streams (functional interfaces don't declare checked exceptions); they tempt developers into the worst anti-pattern, the empty `catch {}` that swallows the error. **Unchecked pros:** clean signatures, friendly to streams/lambdas, no leaky `throws`. **Unchecked cons:** failures are invisible in the type system, so callers may not know to handle them — documentation (Javadoc `@throws`) must compensate. ## The modern trend Many influential Java frameworks (Spring, Hibernate) deliberately use **unchecked** exceptions almost everywhere, wrapping checked ones (e.g. Spring's `DataAccessException` hierarchy wraps `SQLException`). Languages after Java — Kotlin, Scala, C# — dropped checked exceptions entirely. The consensus in much of the industry is that checked exceptions cost more in boilerplate and leaky abstractions than they pay back, so default to unchecked and reserve checked for the rare case where a caller genuinely should be forced to handle a recoverable failure. The opposing view (still defended) is that checked exceptions are a valuable, compiler-verified part of an API contract and overuse of unchecked exceptions hides real recoverable failures. ## Putting it together When you author a custom exception, ask: *Is this a bug the caller should have prevented, or a legitimate runtime condition they might recover from?* Bug or unrecoverable → extend `RuntimeException`. Recoverable and the caller should be forced to deal with it → extend `Exception`. When in doubt in modern code, lean unchecked and document it.

  • Why do checked exceptions cause problems with Java streams and lambdas?
    Standard functional interfaces (Function, Consumer, etc.) don't declare any checked exceptions, so a lambda body that throws a checked exception won't compile inside a stream pipeline. You must catch-and-wrap inside the lambda or use a custom functional interface, which is why checked exceptions are awkward in functional-style code.
  • What does 'leaky abstraction' mean in the context of checked exceptions?
    A low-level checked exception (e.g. SQLException) appearing in a high-level method's throws clause forces callers of an abstract API to know about implementation details they shouldn't care about. The exception 'leaks' through the abstraction boundary, coupling layers together.

saying these in an interview costs you the question

  • Saying checked vs unchecked is about whether the exception is caught at runtime — it's a compile-time enforcement distinction
  • Claiming RuntimeException is not a subclass of Exception (it is)
  • Saying you 'must catch' unchecked exceptions
  • Making every custom exception checked 'to be safe' — this just spreads throws clauses and leaky abstractions
  • Confusing Error with normal exceptions you should handle

context