Skip to content
Java exceptions 6 min read

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.

FamilySuperclassMust be declared/caught?Examples
CheckedException (excluding RuntimeException)YesIOException, SQLException
UncheckedRuntimeException or ErrorNoNullPointerException, 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 throws declaration, documenting them with Javadoc @throws is 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

ScenarioAllowed?
Override throws same checked exceptionYes
Override throws narrower checked exceptionYes
Override throws no checked exceptionYes
Override throws broader checked exceptionNo — compile error
Override throws a new checked exceptionNo — 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 about Throwable, not the checked/unchecked distinction.

  • 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
Last updated June 13, 2026
Was this helpful?