Module 8 · Exceptions, I/O & Resources ⏱ 18 min

Checked vs Unchecked Exceptions

By the end of this lesson you will be able to:
  • Tell a checked exception from an unchecked one and predict the compiler rule each triggers
  • Apply the handle-or-declare rule to method signatures that use throws
  • Choose checked or unchecked deliberately when designing an API

Some failures the compiler refuses to ignore

Call Integer.parseInt("x") and it throws; call code that reads a file and it throws too — yet the compiler treats the two very differently. It never warns you about the parse, but it flatly refuses to let you call the file-reading code without first acknowledging the risk. Why the asymmetry?

The difference is checked versus unchecked exceptions. Checked exceptions are the ones the compiler forces you to plan for; unchecked ones it leaves to your judgement. That distinction shapes every method signature you write, because it decides what a caller is allowed to ignore and what they must handle.

flowchart TD
  M["method body can throw<br/>a checked exception"] --> Q{"caught locally?"}
  Q -->|"yes, try/catch"| OK1["compiles - handled"]
  Q -->|"no"| D{"declared in throws?"}
  D -->|"yes"| OK2["compiles - declared"]
  D -->|"no"| FAIL["compile error<br/>unreported exception"]
  style FAIL fill:#b91c1c,color:#fff
  style OK1 fill:#15803d,color:#fff
  style OK2 fill:#15803d,color:#fff
The handle-or-declare rule. A method that can produce a checked exception must catch it or declare it with throws, or the code will not compile.

Checked, unchecked, and the throws clause

A checked exception is any Exception that is not a RuntimeExceptionIOException, SQLException, and the like. The compiler enforces a strict rule on them called handle or declare: a method whose body can produce a checked exception must either catch it, or announce it with a throws clause in its signature. Do neither and the code will not compile.

An unchecked exception is a RuntimeException (or an Error). These are exempt — no throws clause required, no compulsion to catch. The reasoning is that they usually mark a programming bug, and forcing every caller to guard against a null reference would drown real signatures in noise.

The throws clause is therefore a contract with the caller: I may fail this way, and you must decide what to do about it. It is information the compiler can verify, which is exactly why checked exceptions survive into serious APIs.

Main.java — a checked exception declared with throws, then caught at the caller. Real, compiling Java.
public class Main {
    // A method that can fail with a CHECKED exception must advertise it.
    static String load(int id) throws java.io.IOException {
        if (id < 0) {
            throw new java.io.IOException("bad id: " + id);
        }
        return "row-" + id;
    }

    public static void main(String[] args) {
        try {
            System.out.println(load(-1));
        } catch (java.io.IOException e) {
            System.out.println("handled: " + e.getMessage());
        }
    }
}

Checked exceptions climb the stack too

A checked exception propagates up the call stack just like an unchecked one — every frame either catches it or re-declares it. If fetch() declares throws IOException, then any method that calls fetch() and does not catch it must itself declare throws IOException. The obligation fans outward toward the caller until somebody handles it.

If that chain reaches main uncaught, the program terminates and prints the stack trace, exactly as with an unchecked failure. The difference is never whether the failure can happen; it is whether the compiler made you think about it ahead of time.

flowchart LR
  F["fetch()<br/>throws IOException"] -->|"propagates"| L["lookup()<br/>declares throws IOException"]
  L -->|"propagates"| MAIN["main()<br/>catches it"]
  MAIN --> C["handled here"]
  style MAIN fill:#c2410c,color:#fff
A checked exception handed up the call stack: each frame either catches it or declares it with throws, until one handles it.

Reading a throws clause

A signature can list several checked types: void save() throws IOException, SQLException. The caller must then handle or declare each one, though catching a common supertype such as Exception covers them all at once.

Catching a supertype is convenient but blunt — you lose the ability to react differently to distinct failures. When it matters, catch each type in its own clause and respond appropriately; when it does not, a single broad catch is acceptable at a boundary such as a top-level request handler. The throws list documents possibility; your catch clauses encode policy.

Main.java — NumberFormatException is unchecked, so parse needs no throws clause. Real, compiling Java.
public class Main {
    // NumberFormatException is unchecked, so no throws clause is required.
    static int parse(String s) {
        return Integer.parseInt(s);
    }

    public static void main(String[] args) {
        System.out.println(parse("42"));
        try {
            System.out.println(parse("abc"));
        } catch (NumberFormatException e) {
            System.out.println("not numeric");
        }
    }
}

Why unchecked failures skip the rule

When you read s.length() you do not want a compiler lecture about NullPointerException on every line — the right fix is to ensure s is not null in the first place, not to wrap each call in a try/catch. Unchecked exceptions exist for failures that signal fix the code, not plan for this at runtime.

That is why bugs like a bad array index, a null dereference, or an arithmetic overflow are unchecked. They describe a mistake a developer should correct, so forcing callers to declare them would only obscure signatures without making anyone safer. Reserve checked exceptions for conditions a caller can genuinely react to.

flowchart TD
  Q{"failure cause?"} --> BUG["programming bug<br/>null, bad index, bad arg"]
  Q --> EXT["external or recoverable<br/>file, network, database"]
  BUG --> UC["throw UNCHECKED<br/>a RuntimeException"]
  EXT --> CH["throw CHECKED<br/>declare with throws"]
  style UC fill:#b45309,color:#fff
  style CH fill:#3776ab,color:#fff
A design rule of thumb: programming bugs become unchecked exceptions; external, recoverable conditions become checked ones.

Choosing what to throw

When you design a method, the exception strategy is part of the API. The useful question is: can the caller do anything about it? If a flaky network or a missing file fails, the caller might retry, fall back to a cache, or prompt the user — so model it as a checked exception and force that decision on them.

If the failure is a programmer mistake — a null where an object was required, an argument that violates a precondition — recovery at runtime is meaningless. Throw an unchecked exception instead, so it crashes loudly during testing rather than hiding behind boilerplate handling. Checked for the recoverable and external, unchecked for the bug: that single rule keeps your signatures honest.

Exercise

What does this print? readConfig declares throws IOException and always throws. Predict the exact output, two lines.

public class Main {
    static int readConfig() throws java.io.IOException {
        throw new java.io.IOException("config missing");
    }

    public static void main(String[] args) {
        try {
            int v = readConfig();
            System.out.println(v);
        } catch (java.io.IOException e) {
            System.out.println("handled: " + e.getMessage());
        }
        System.out.println("after");
    }
}
Exercise

What does this print? lookup delegates to fetch, which throws only for negative ids. Predict the exact output, two lines.

public class Main {
    static String fetch(int id) throws java.io.IOException {
        if (id < 0) {
            throw new java.io.IOException("bad id");
        }
        return "row-" + id;
    }

    static String lookup(int id) throws java.io.IOException {
        return fetch(id);
    }

    public static void main(String[] args) {
        try {
            System.out.println(lookup(5));
            System.out.println(lookup(-1));
        } catch (java.io.IOException e) {
            System.out.println("caught: " + e.getMessage());
        }
    }
}
Exercise

What does this print? parse needs no throws clause. Predict the exact output, two lines.

public class Main {
    static int parse(String s) {
        return Integer.parseInt(s);
    }

    public static void main(String[] args) {
        try {
            System.out.println(parse("42"));
            System.out.println(parse("hi"));
        } catch (NumberFormatException e) {
            System.out.println("not a number");
        }
    }
}
Exercise

A method calls Thread.sleep(100), which declares throws InterruptedException. The method does not catch it. What must it do?

Exercise

Inside a try body that only does int x = 5;, you write catch (IOException e) { ... }. What happens?

Recap

  • A checked exception is an Exception that is not a RuntimeException — the compiler forces you to catch or declare it.
  • An unchecked exception is a RuntimeException or Error — no throws clause required, no compulsion to catch.
  • The handle-or-declare rule makes a throws clause a contract: callers are told exactly what they must plan for.
  • Catching a supertype handles all its subtypes at once, at the cost of reacting to each the same way.
  • Design with intent: checked for recoverable external conditions, unchecked for programming bugs.

Next you will close a resource safely with try-with-resources, and see what happens when a cleanup step itself fails.

Checkpoint quiz

Which of these is an UNCHECKED exception?

Why declare throws IOException on a method that reads a file?

Go deeper — technical resources