Exception Rules in Java
-
Last Updated: November 4, 2024
-
By: javahandson
-
Series
Learn Java in a easy way
The exception rules in Java decide what a method may declare in its throws clause once a child class shows up. Learn why overloads have no rules, what a child method may declare, and why unchecked types walk free.
You write a subclass, add throws Exception to an overridden method, and the compiler refuses. Nothing looks wrong, and yet the build fails.
That wall you just hit has a name. The exception rules in Java govern what a method may declare in its throws clause. They kick in with overloads and with child classes.
The good news arrives early. Overloaded methods have no rules at all, so half this topic disappears in a single section.
Overriding is the half that matters. A child method may promise the same failures as its parent, or fewer, but never more.
This is part 5, the final part of our five-part series. Part 3 covered throw and throws, and part 4 covered custom and chained exceptions.
We start with the reason behind the rules, then work through every legal and illegal combination. Here is the plan:
You need throws from part 3 and a working idea of inheritance. Everything else we build here.
Rules learnt by heart work until you forget one. The reason behind them sticks for good, so let us start there.
Look at this line of code and ask what the compiler knows:
Report report = new PdfReport(); // reference type Report, object type PdfReport report.export();
The compiler sees a Report reference and nothing more. Which object arrives at runtime depends on the code path, and it could be any subclass ever written.
So the compiler checks the throws clause of Report.export. That clause tells it which catch blocks you need.
Think of the parent method as a contract. It promises callers that it fails in these ways, and in no others.
Now picture a child that declares a brand new checked type. Your catch block would miss it, and the compiler could never warn you.
Java closes that hole at the source. A child may keep the promise or make a smaller one, and the compiler rejects anything bigger.
One boundary matters before we go further. Every rule in this article applies to checked exceptions and nothing else.
Unchecked types stay out of the catch-or-declare rule, so widening them costs nothing. Part 1 of this series drew that line between the two families.
Keep this split in mind and half the muddle goes away.
Here is the whole section in one line. Overloads have no exception rules at all.
Why so simple? Because overloads only share a name. Different parameters make them genuinely different methods, and the compiler picks one at the call site.
Your caller therefore knows exactly which overload it invoked. Nothing can surprise it, so Java needs no rule here.
This class overloads save three times. One throws a checked exception, one throws a different checked exception, and one declares nothing.
package com.javahandson;
import java.io.IOException;
public class OverloadRules {
static void save(String text) throws IOException {
System.out.println("Saved text: " + text);
}
static void save(int id) throws IllegalAccessException {
System.out.println("Saved id: " + id);
}
static void save(double amount) { // no throws clause at all
System.out.println("Saved amount: " + amount);
}
public static void main(String[] args) throws IOException, IllegalAccessException {
save("invoice");
save(77);
save(9.99);
}
}
// Output:
// Saved text: invoice
// Saved id: 77
// Saved amount: 9.99Three throws clauses in one class, with no link between them. IOException and IllegalAccessException meet only at Exception itself.
Look at main for the practical effect. It declares both checked types, because it calls both overloads.
Swap any of those for unchecked types and nothing changes. Mixing checked and unchecked across overloads works too.
One trap hides here, and interviewers love it. A throws clause is not part of the method signature.
static void save(String text) throws IOException { }
static void save(String text) throws SQLException { }
// error: method save(String) is already defined in class OverloadRulesBoth methods take one String, so Java sees one method declared twice. Different exceptions change nothing.
The same applies to return types. Only the name and the parameter list distinguish one overload from another.
Overriding is where the real rules live. A child method redefines a parent method, so callers cannot tell which version runs.
Three moves stay legal for checked exceptions:
Two moves stop the build. Declaring a supertype fails, and declaring an unrelated checked type fails too.
Notice the pattern. Everything the child declares must fit underneath something the parent already promised.
Start with the obvious case. Repeating the parent’s exception always compiles.
package com.javahandson;
import java.io.IOException;
class Report {
void export() throws IOException {
System.out.println("Report exported");
}
}
class PdfReport extends Report {
@Override
void export() throws IOException { // identical clause, always legal
System.out.println("PDF exported");
}
}
public class SameException {
public static void main(String[] args) throws IOException {
Report report = new PdfReport();
report.export();
}
}
// Output: PDF exportedMain declares IOException because Report.export declares it. Whether a PdfReport or a plain Report turns up makes no difference to the compiler.
Now let the child promise something smaller. FileNotFoundException extends IOException, so it fits underneath.
import java.io.FileNotFoundException;
import java.io.IOException;
class Report {
void export() throws IOException {
System.out.println("Report exported");
}
}
class PdfReport extends Report {
@Override
void export() throws FileNotFoundException { // narrower, perfectly legal
System.out.println("PDF exported");
}
}
// compiles cleanlyAny caller catching IOException still catches a FileNotFoundException, because catch matches subclasses. Nobody’s code breaks.
You may also list more than one type. Each must fit under a parent type. So throws FileNotFoundException, EOFException works fine here, since both extend IOException.
A child can drop the throws clause completely. This surprises beginners, and it follows straight from the contract idea.
class Report {
void export() throws IOException {
System.out.println("Report exported");
}
}
class PdfReport extends Report {
@Override
void export() { // declares nothing, still legal
System.out.println("PDF exported");
}
}
public class NoException {
public static void main(String[] args) throws IOException {
Report report = new PdfReport();
report.export(); // main still needs IOException here
PdfReport pdf = new PdfReport();
pdf.export(); // no handling needed with this reference type
}
}
// Output:
// PDF exported
// PDF exportedRead those last two calls carefully, because they show the whole idea in action. The reference type decides what the compiler demands.
Through a Report reference, IOException remains on the table. Through a PdfReport reference, the compiler sees a method that promises nothing.
Widening is the classic failure. Exception sits above IOException, so the child promises more than the parent did.
class Report {
void export() throws IOException {
}
}
class PdfReport extends Report {
@Override
void export() throws Exception { // too broad
}
}
// error: export() in PdfReport cannot override export() in Report
// overridden method does not throw ExceptionThat second line of the error says everything. Nothing in the parent covers Exception, so the compiler stops right there.
Picture the caller once more. It wrote catch (IOException e) and would now face an Exception nobody handles.
Broader is not the only problem. A checked type from a completely different branch fails just as hard.
class PdfReport extends Report {
@Override
void export() throws SQLException { // unrelated to IOException
}
}
// error: export() in PdfReport cannot override export() in Report
// overridden method does not throw SQLExceptionSQLException is neither broader nor narrower here. It has no place under IOException, and that alone rules it out.
So forget the words broader and narrower for a moment. The real test asks whether each declared type sits under something the parent promised.
Here is the strictest case of all, and it catches people out constantly.
class Report {
void export() { // no throws clause
}
}
class PdfReport extends Report {
@Override
void export() throws IOException { // no room for any checked exception
}
}
// error: export() in PdfReport cannot override export() in Report
// overridden method does not throw IOExceptionAn empty throws clause leaves the child zero space. No checked exception can fit under a promise that lists nothing.
This bites hard when you implement an interface such as Runnable. Its run method declares nothing, so your implementation cannot let a checked exception escape.
What do you do then? Wrap it in an unchecked exception, exactly as part 4 showed with the (String, Throwable) constructor.
Everything above concerned checked exceptions. Unchecked types walk straight past all of it.
package com.javahandson;
class Report {
void export() throws ArithmeticException { // unchecked
System.out.println("Report exported");
}
}
class PdfReport extends Report {
@Override
void export() throws RuntimeException { // broader, and still fine
throw new IllegalStateException("Template missing");
}
}
public class UncheckedRules {
public static void main(String[] args) { // no throws, no try-catch
Report report = new PdfReport();
report.export();
}
}
// Output:
// Exception in thread "main" java.lang.IllegalStateException: Template missing
// at com.javahandson.PdfReport.export(UncheckedRules.java:12)
// at com.javahandson.UncheckedRules.main(UncheckedRules.java:18)RuntimeException sits above ArithmeticException, so this widens the clause. The compiler waves it through anyway.
Try any mix you like with unchecked types. Broad, narrow, or a class the parent never named, and it all compiles.
The reason follows from part 1. Nobody has to catch an unchecked exception, so no caller can break when a new one appears.
The rule exists to protect callers who wrote catch blocks under compiler orders. Unchecked exceptions never carried that obligation.
Errors work the same way. OutOfMemoryError and friends stay outside the rules for exactly the same reason.
One point ties this whole article together. It splits people who know the topic from people who learnt it by rote.
The rules govern what a method declares. They say nothing about what it throws at runtime.
class PdfReport extends Report {
@Override
void export() { // declares nothing at all
throw new IllegalStateException("boom"); // yet it throws happily
}
}That method declares no exception and still ends the program. Nothing in the language stops a method from throwing unchecked types.
Every case for a child method fits into one lookup table. The parent method here declares whatever the first column says.
| Parent declares | Child declares | Compiles? | Reason |
|---|---|---|---|
IOException |
IOException |
Yes | Identical promise |
IOException |
FileNotFoundException |
Yes | Narrower, fits underneath |
IOException |
nothing | Yes | A smaller promise is always safe |
IOException |
FileNotFoundException, EOFException |
Yes | Both sit under IOException |
IOException |
Exception |
No | Broader than the parent promised |
IOException |
SQLException |
No | Unrelated checked type |
| nothing | IOException |
No | Zero room for any checked type |
| anything | any unchecked type | Yes | The rules ignore unchecked exceptions |
One sentence covers the entire table. Each checked type in the child must be the same as, or a subclass of, some type the parent declared.
Both words start with the same six letters, and beginners mix them up under interview pressure. Here they are side by side.
| Aspect | Overloading | Overriding |
|---|---|---|
| Where it happens | One class, or a subclass | Between a parent and a child class |
| Method signature | Same name, different parameters | Same name, same parameters |
| Which one runs | The compiler picks it | The JVM picks it at runtime |
| Exception rules | None at all | Checked exceptions must not widen |
| throws clause | Fully independent per method | Limited by the parent method |
| Unchecked exceptions | Unrestricted | Unrestricted |
Remember the one line that matters. Overloading gives you freedom, while overriding hands you a contract to honour.
Implementing an interface method follows identical rules. The interface declares the contract, and your class must stay inside it.
interface Storage {
void write(String data) throws IOException;
}
class MemoryStorage implements Storage {
@Override
public void write(String data) { // narrowing to nothing, legal
System.out.println("Wrote " + data);
}
}This pattern shows up all over real code. An in-memory class cannot fail with an IOException, so it declares none.
Now a trickier case. One class implements two interfaces that declare the same method with different checked exceptions.
interface FileSource {
void load() throws IOException;
}
interface DatabaseSource {
void load() throws SQLException;
}
class HybridSource implements FileSource, DatabaseSource {
@Override
public void load() { // must satisfy both, so no checked exception fits
System.out.println("Loaded");
}
}Your method has to honour both contracts at once. IOException breaks the database contract, and SQLException breaks the file one.
Only the overlap survives, and these two share nothing. So the class declares no checked type at all.
A static method in a child class does not override the parent version. It hides it, which is a different mechanism.
class Report {
static void init() throws IOException {
}
}
class PdfReport extends Report {
static void init() throws Exception { // hiding, and the same limit applies
}
}
// error: init() in PdfReport cannot hide init() in Report
// overridden method does not throw ExceptionThe mechanism changed, and the limit did not. Java applies the same throws rule to hiding as it does to overriding.
Two more cases need no rule at all. A private or final method cannot be overridden, so nothing constrains it.
Constructors are never inherited, so they are never overridden either. That frees them from everything above.
class Report {
Report() throws IOException {
}
}
class PdfReport extends Report {
PdfReport() throws IOException, SQLException { // extra exception, and it compiles
super();
}
}Notice the direction here. A child constructor must declare the checked exceptions of the super() call it makes, and it may add more of its own.
That is the exact opposite of the rule above. Constructors widen freely, while child methods must not.
A lambda is just a method body for one interface method. So the throws clause of that interface still applies.
// Runnable.run declares nothing, so this will not compile
Runnable task = () -> Files.readString(Path.of("config.txt"));
// error: unreported exception IOException; must be caught or declared to be thrown
// Callable.call declares Exception, so the same body is fine here
Callable<String> job = () -> Files.readString(Path.of("config.txt"));Same body, two results. Runnable promises no checked exception, while Callable promises Exception.
This trips up a lot of stream code too. The lambda you pass to map or forEach cannot let a checked exception out, so you catch it and wrap it inside the lambda.
Some coders write throws Exception on a base method so subclasses never hit the rule. It works, and it wrecks all the rest.
// Poor: every caller now handles a type that means nothing
abstract class Task {
abstract void run() throws Exception;
}
// Better: name what this family of tasks can actually fail with
abstract class Task {
abstract void run() throws TaskFailedException;
}The broad version pushes catch (Exception e) into every caller. Part 3 explained why that hides real failures.
Plenty of people believe a child method cannot throw anything new at runtime. Only the declaration faces restrictions.
A child that declares nothing can still throw ten different unchecked exceptions. The compiler never inspects the body for those.
Assigning a child object to a parent reference does not shrink your obligations. The reference type wins every time.
Report report = new PdfReport(); // PdfReport.export declares nothing report.export(); // you still handle IOException here PdfReport pdf = new PdfReport(); pdf.export(); // now the compiler asks for nothing
Same object, same method, two different demands from the compiler. Only the declared type of the variable changed.
You override a method that declares nothing, and your body needs to call something throwing IOException. Beginners then edit the parent class.
@Override
public void run() {
try {
Files.readString(Path.of("config.txt"));
} catch (IOException e) {
throw new ConfigLoadException("Cannot read config.txt", e); // wrap it
}
}Catch the checked exception and wrap it in an unchecked one, keeping the original as the cause. Part 4 covered that pattern in detail.
Leave out @Override and a typo in the method name creates a brand new method instead. No rule applies, no error appears, and your code silently calls the parent version.
Add the annotation to every method you intend to override. The compiler then checks your intent, exception rules included.
Let us build something that shows why the rule earns its keep. We need a validator family with three implementations.
The base class declares throws IOException, since a validator might read a rules file. One child narrows that to FileNotFoundException, and another drops it entirely.
Then we loop over all three through a single parent-typed array. One catch block should cover every one of them.
package com.javahandson;
import java.io.FileNotFoundException;
import java.io.IOException;
class Validator {
// the contract: validation may fail with an IOException
void validate(String input) throws IOException {
System.out.println("Base check passed for [" + input + "]");
}
}
class StrictValidator extends Validator {
@Override
void validate(String input) throws FileNotFoundException { // narrower
if (input.isBlank()) {
throw new FileNotFoundException("No rules file for a blank input");
}
System.out.println("Strict check passed for [" + input + "]");
}
}
class LenientValidator extends Validator {
@Override
void validate(String input) { // nothing at all
System.out.println("Lenient check passed for [" + input + "]");
}
}
public class ValidatorApp {
public static void main(String[] args) {
Validator[] validators = {
new Validator(),
new StrictValidator(),
new LenientValidator()
};
for (Validator validator : validators) {
try {
validator.validate("");
} catch (IOException e) { // one catch covers the whole family
System.out.println(validator.getClass().getSimpleName()
+ " failed: " + e.getMessage());
}
}
}
}
// Output:
// Base check passed for []
// StrictValidator failed: No rules file for a blank input
// Lenient check passed for []Walk the loop three times. The base validator prints its line and returns normally.
StrictValidator throws a FileNotFoundException on the blank input. Our catch block asks for IOException, and FileNotFoundException extends it, so the match works.
LenientValidator declares nothing and simply prints. The catch block sits there unused, which costs nothing.
Now spot the payoff hiding in that catch line:
Try breaking it yourself. Add throws Exception to any child and the build fails before the loop ever runs.
A: There are none. Overloaded methods differ in their parameter lists, so Java treats them as separate methods. Each one declares whatever checked or unchecked exceptions it likes, and the compiler picks the right overload at the call site.
A: No. If the parent declares IOException, the child cannot declare Exception, because Exception sits above IOException. The compiler reports that the overridden method does not throw that type. A child may only declare the same type, a subclass of it, or fewer types.
A: Because callers usually hold a parent-type reference and write catch blocks based on the parent’s throws clause. A wider child clause would let an exception escape with no catch block for it. The compiler could never warn you. Narrowing stays safe, since a catch block also matches subclasses.
A: Yes, and this is very common. Dropping the throws clause makes a smaller promise than the parent, which never breaks a caller. Callers who hold a parent reference still handle the parent’s types. The reference type decides what the compiler asks for.
A: The child cannot declare any checked exception, since nothing in the parent covers it. This shows up when implementing Runnable, whose run method declares nothing. Catch the checked exception inside the method and wrap it in an unchecked one instead.
A: No. A child method may declare any RuntimeException or Error. Broad, narrow, or with no link at all, the code still compiles. The rules exist to protect callers who wrote catch blocks under compiler orders, and unchecked exceptions never carried that obligation.
A: Yes, as long as it is unchecked. The rules cover the throws clause, not the method body. A method that declares nothing can still throw IllegalStateException at runtime. Checked exceptions inside the body remain subject to the usual catch-or-declare rule.
A: No. A throws clause is not part of the method signature, so two methods with the same name and the same parameters clash. The compiler reports that the method is already defined in the class. Return types behave the same way.
A: Yes. A static method in a subclass hides the parent version rather than overriding it. Java applies the same throws limit to hiding. Private and final methods are different, because no subclass can redefine them at all.
A: No, because constructors are never inherited or overridden. A child constructor must declare the checked types thrown by its super() call. It may also add more of its own. That is the opposite of the overriding rule.
A: The method must honour both contracts. So it may declare only checked types that fit under both throws clauses. When the two types share nothing, as with IOException and SQLException, the method can declare no checked exception at all.
Let us wrap up what we covered. Overloads carry no exception rules. Different parameter lists make them separate methods.
Overriding is where the restriction lives. Each checked exception in the child must be the same as, or a subclass of, something the parent already declared.
Three moves stay legal: the same type, a narrower type, or nothing at all. Widening fails, an unrelated checked type fails, and a parent with an empty clause leaves the child no room.
Unchecked exceptions ignore every one of these rules. Remember too that the rules police the throws clause, never the method body.
Behind all of it sits one idea. A caller holding a parent reference wrote its catch blocks from the parent contract, and no subclass may quietly break that.
This article closes our five-part exception series. You now know what exceptions are, how to catch them, and how to throw them. You can write and chain your own, and you know the rules a child class adds on top.