skip to content

What are the rules for declared exceptions when overriding a method, and why do they exist?

level: seniorimportance: should knowfreq 50%

answer

  1. Checked: same, subtype, fewer, or none
  2. Cannot add or broaden checked exceptions
  3. Unchecked: no restriction at all
  4. Reason = caller's compile-time contract / substitutability
  5. Multiple supertypes -> intersection of allowed checked

basics

~20 s

An overriding method may throw the same checked exceptions, narrower (subtype) ones, or fewer - but it may not declare new or broader checked exceptions than the method it overrides. Unchecked (runtime) exceptions are unrestricted.

solid answer

~50 s

When overriding, the subclass method's `throws` clause for CHECKED exceptions must be a subset (by type) of the parent's: same types, subtypes of them, fewer of them, or none at all. It cannot add a new checked exception or broaden one (e.g. parent throws `IOException`, child cannot throw `Exception`). Unchecked exceptions - `RuntimeException` and `Error` and their subtypes - are not constrained at all; an override can throw any of them regardless of the parent's clause. The reason is substitutability: a caller invoking the method through a parent-typed reference only compiled against the parent's declared checked exceptions and prepared to handle exactly those. If the override could throw a brand-new checked exception, the caller would face an exception it never declared or caught - defeating the compiler's checked-exception guarantee. Narrowing or dropping exceptions is always safe because the caller is already prepared for the wider set.

code

java · 24 lines
java
import java.io.*;

class Reader {
    String read() throws IOException { return ""; }
}

class NarrowerReader extends Reader {
    @Override
    String read() throws FileNotFoundException { return ""; } // OK: subtype of IOException
}

class QuietReader extends Reader {
    @Override
    String read() { return ""; }                              // OK: declares nothing
}

class RuntimeReader extends Reader {
    @Override
    String read() { throw new IllegalStateException(); }      // OK: unchecked, unrestricted
}

// class BadReader extends Reader {
//     @Override String read() throws Exception { return ""; } // COMPILE ERROR: broader checked
// }

go deeper

for a junior

Knows roughly that an override 'cannot throw more exceptions' than the parent, even if fuzzy on checked vs unchecked.

for a middle

States the rule correctly for checked exceptions (same/narrower/fewer/none) and knows unchecked are exempt.

for a senior

Explains the substitutability/contract rationale, the type-assignability (not count) nuance, and handles the unchecked-exemption confidently.

for a principal

Discusses the multiple-supertype intersection, the design tension between checked exceptions and inheritance/lambdas, and how this rule shapes API evolution and exception-translation strategies.

## Background: checked vs unchecked exceptions Java splits exceptions into two families: - **Checked exceptions** (subtypes of `Exception` but NOT of `RuntimeException`, e.g. `IOException`, `SQLException`): the compiler *forces* the caller to either catch them or declare them in its own `throws` clause. They are part of the method's compile-time contract. - **Unchecked exceptions** (`RuntimeException` and `Error` and their subtypes, e.g. `NullPointerException`, `IllegalArgumentException`): the compiler imposes no such requirement; they can be thrown anywhere without declaration. ## The overriding rule When a subclass **overrides** a method, the override's `throws` clause is constrained **only for checked exceptions**: > The override may declare the **same** checked exceptions, **subtypes** of them, a **subset** of them, or **none** - but it may NOT introduce a **new** checked exception or a **broader** (supertype) one. Unchecked exceptions are entirely **unconstrained** - the override may throw any `RuntimeException`/`Error` it wants, no matter what the parent declared. ```java class Reader { String read() throws IOException { ... } } class BufferedReaderX extends Reader { @Override String read() throws FileNotFoundException { ... } // OK: subtype of IOException (narrower) } class SilentReader extends Reader { @Override String read() { ... } // OK: throws nothing (fewer) } class BadReader extends Reader { @Override String read() throws Exception { ... } // COMPILE ERROR: broader than IOException } ``` ## Why the rule exists - substitutability The governing principle is **Liskov substitutability**: an object of the subclass must be usable anywhere the superclass is expected, without surprising the caller. Consider a caller holding a `Reader` reference that actually points to a subclass instance: ```java Reader r = getSomeReader(); // could be any subclass try { r.read(); } catch (IOException e) { ... } // caller compiled against Reader's contract ``` The caller only knows the **parent** contract: `read()` throws `IOException`. The compiler verified the caller handles `IOException`. If a subclass override were allowed to throw, say, a *new* `ParseException` (checked) that is not an `IOException`, that exception could propagate out of `r.read()` at runtime, yet the caller never caught or declared it. That would silently violate the entire promise of checked exceptions - the compiler's guarantee that 'every checked exception is accounted for'. So the language **forbids** widening or adding checked exceptions. Conversely, **narrowing or dropping** checked exceptions is always safe: the caller is *already* prepared to handle the parent's full (wider) set, so throwing fewer or more-specific ones can never surprise it. Unchecked exceptions are exempt precisely because they were never part of the compile-time contract - callers were never promised they wouldn't occur, so an override adding one breaks no promise. ## Subtlety: it is per-type subset, not count The rule is about **type assignability**, not the number of entries. An override may throw `FileNotFoundException` (a subtype) even though the parent listed only `IOException`. It may also declare *fewer* exceptions. What it cannot do is name a checked type that is not assignable to one of the parent's declared checked types. ## Interaction with interfaces and multiple supertypes When a method overrides/implements from **multiple** supertypes (e.g. a class implementing two interfaces that both declare the method with different `throws`), the override may only throw checked exceptions allowed by **all** of them - effectively the *intersection*. This can be more restrictive than any single parent.

  • Can an overriding method throw a RuntimeException even if the parent declares no exceptions at all?
    Yes. Unchecked exceptions (RuntimeException/Error and subtypes) are never part of the checked-exception contract, so an override may throw them freely regardless of the parent's throws clause.
  • A class implements two interfaces whose method declarations throw IOException and SQLException respectively. What can the implementing method's throws clause contain?
    Only checked exceptions allowed by BOTH - effectively the intersection. Since IOException and SQLException are unrelated, the implementation can throw neither as checked; it must throw none (or only unchecked exceptions, or subtypes common to both, of which there are none here).

A parent's warranty says 'this product may fail in ways A or B.' A replacement model can promise 'only fails in way A' or 'never fails' - customers are happy, they were braced for A and B. But it cannot suddenly announce 'also fails in way Z' that the warranty never mentioned, because customers made no contingency for Z.

saying these in an interview costs you the question

  • Claiming an override can throw any exception it likes - true for unchecked, false for checked.
  • Saying the override must throw exactly the same exceptions - it may throw fewer or narrower ones.
  • Forgetting that the count doesn't matter, only type-assignability to a parent-declared checked type.
  • Ignoring the multiple-supertype intersection case.

context