Exceptions & Method Overriding
When you override a method in Java, you are not free to do whatever you like with the throws clause. Java enforces specific rules about which exceptions an overriding method may declare — rules that protect callers who rely only on the parent class’s contract. Understanding these rules saves you from confusing compile errors and, more importantly, from writing brittle code that surprises consumers of your API.
The Core Rule
An overriding method may not declare new or broader checked exceptions than the method it overrides. It can, however:
- Declare no checked exceptions at all.
- Declare a narrower (subclass) checked exception.
- Declare any unchecked (runtime) exception — freely.
Think of it this way: a caller that holds a reference of the parent type trusts the parent’s throws contract. If the overriding method silently added a broader checked exception, that caller would have no way to handle it — the compiler would miss it entirely.
Checked vs Unchecked — A Quick Recap
Before diving into examples, it helps to recall the two families. See Exception Handling for full details.
| Family | Superclass | Must be declared/caught? | Examples |
|---|---|---|---|
| Checked | Exception (excluding RuntimeException) | Yes | IOException, SQLException |
| Unchecked | RuntimeException or Error | No | NullPointerException, IllegalArgumentException |
The overriding rules apply only to checked exceptions. Unchecked exceptions are always exempt.
Case 1 — Declaring a Narrower Checked Exception (Allowed)
import java.io.IOException;
import java.io.FileNotFoundException; // subclass of IOException
class FileProcessor {
// Parent declares the broader IOException
void process() throws IOException {
System.out.println("Processing file...");
}
}
class StrictFileProcessor extends FileProcessor {
// Child narrows to FileNotFoundException — perfectly legal
@Override
void process() throws FileNotFoundException {
System.out.println("Strict processing...");
}
}
FileNotFoundException is a subclass of IOException, so any caller already prepared to handle IOException can handle FileNotFoundException too. The contract is preserved.
Case 2 — Declaring No Checked Exception (Allowed)
import java.io.IOException;
class FileProcessor {
void process() throws IOException { }
}
class SafeProcessor extends FileProcessor {
// Removes the checked exception entirely — legal
@Override
void process() {
System.out.println("This implementation never throws checked exceptions.");
}
}
Removing a checked exception from the throws clause is always safe — the child is simply promising even less risk than the parent.
Case 3 — Declaring a Broader Checked Exception (Compile Error)
import java.io.IOException;
import java.io.FileNotFoundException;
class FileProcessor {
void process() throws FileNotFoundException { } // declares narrow exception
}
class BrokenProcessor extends FileProcessor {
@Override
void process() throws IOException { // COMPILE ERROR: IOException is broader
System.out.println("This will not compile.");
}
}
Output:
error: process() in BrokenProcessor cannot override process() in FileProcessor
overridden method does not throw IOException
A caller holding a FileProcessor reference only knows about FileNotFoundException. If BrokenProcessor could sneak in IOException, that broader exception would go unhandled.
Case 4 — Declaring a New Unchecked Exception (Always Allowed)
Unchecked exceptions are not part of the method contract from the compiler’s perspective, so an overriding method can throw any RuntimeException it likes.
import java.io.IOException;
class DataReader {
void read() throws IOException { }
}
class FastDataReader extends DataReader {
@Override
void read() throws IOException, IllegalStateException { // fine — unchecked
if (Math.random() < 0.1) {
throw new IllegalStateException("Reader not initialized");
}
System.out.println("Reading data...");
}
}
Tip: Even though unchecked exceptions don’t require a
throwsdeclaration, documenting them with Javadoc@throwsis good practice so callers know what to expect.
Case 5 — Parent Declares No Exception; Child Wants to Add One
When the parent method declares no checked exceptions, the overriding child also cannot add any checked exception. It can only add unchecked ones.
class Printer {
void print() { } // no throws clause
}
class FilePrinter extends Printer {
// @Override
// void print() throws IOException { } // COMPILE ERROR
@Override
void print() throws RuntimeException { } // unchecked — OK
}
This is the rule that trips up developers most often: you cannot retroactively introduce a checked exception just because your implementation happens to need file I/O.
Warning: If you find yourself needing to throw a checked exception from an override whose parent declares none, wrap the checked exception in an unchecked one (e.g.,
throw new RuntimeException(e)). This is a deliberate design choice — make it visible in code review.
Summary Table
| Scenario | Allowed? |
|---|---|
| Override throws same checked exception | Yes |
| Override throws narrower checked exception | Yes |
| Override throws no checked exception | Yes |
| Override throws broader checked exception | No — compile error |
| Override throws a new checked exception | No — compile error |
Override throws any unchecked (RuntimeException) | Yes (always) |
Practical Pattern — Interface + Implementation
This pattern shows up in real-world code all the time. An interface often declares a broad exception so that various implementations have flexibility; a concrete class then narrows or removes it.
import java.io.IOException;
interface DataExporter {
void export() throws IOException;
}
// Implementation that writes to memory — never throws IOException
class InMemoryExporter implements DataExporter {
@Override
public void export() { // narrowed to no exception
System.out.println("Exporting to memory buffer...");
}
}
// Implementation that actually writes a file
class FileExporter implements DataExporter {
@Override
public void export() throws IOException { // same exception — fine
System.out.println("Exporting to file...");
// real file I/O would go here
}
}
Notice how callers programming to the DataExporter interface still need to handle IOException (since the interface declares it), even when the actual runtime object is an InMemoryExporter that never throws. The narrowing is a detail of the concrete type.
Under the Hood
Why does Java impose these rules at all? It comes down to the substitution principle (Liskov Substitution Principle). If you have a variable of type Parent, you must be able to swap in any Child without breaking existing code. The throws clause is part of the method signature from the caller’s perspective. Widening the checked exception set would mean a caller that correctly handles only the parent’s exceptions would suddenly receive an exception it has no handler for — a silent runtime failure.
The compiler enforces this during the overriding check phase of compilation. It compares the resolved exception sets of the two method descriptors (the parent’s and the child’s). Unchecked exceptions are excluded from this check because they bypass the compile-time safety net entirely — the compiler never enforces their handling.
At the bytecode level, the throws clause is stored in the method’s Exceptions attribute in the .class file. The JVM itself does not enforce checked exceptions at runtime; that is purely a compiler guarantee. You can even use reflection to call a method that throws a checked exception without catching it — the JVM will propagate it normally.
Note: This is why some frameworks (like Lombok’s
@SneakyThrows) can “sneak” checked exceptions past the compiler — they are invisible to the JVM, which only cares aboutThrowable, not the checked/unchecked distinction.
Related Topics
- Method Overriding — the full rules for overriding methods, including return types and access modifiers
- Exception Handling — the complete guide to try-catch, throws, and the exception hierarchy
- throws Keyword — how to declare checked exceptions on methods and constructors
- Custom Exceptions — creating your own exception hierarchy for expressive error handling
- Runtime Polymorphism — how the JVM selects the right overriding method at runtime
- Abstract Class — defining abstract methods whose overrides must respect the exception contract