← Fundamentals
Guided lesson 7 questions · Java only

Java checked and unchecked exceptions

Which exceptions javac makes you catch or declare, and how to keep a failure when the compiler is in the way.

Java sorts exceptions by class. An exception whose class is RuntimeException, Error, or a subclass of one is unchecked: javac asks nothing about it. Every other exception is checked: a method that can throw one must catch it or declare it with throws. Inside a lambda, where a checked exception cannot be declared, wrap it in an unchecked exception that keeps it as the cause; never replace it with a default value.

Written for Java.

Before you begin

Why this matters

Most Java code you read either declares an exception, catches one, or works around the rule. If you know which exceptions javac checks, you can read its two error messages, choose a parent class for your own exceptions, and spot code that hides a failure only to stop a compiler error.

What this helps you reason about

  • Say whether an exception is checked from its superclasses, and fix the javac errors unreported exception and is never thrown.
  • Order catch blocks, write a multi-catch, and give your own exception class a cause.
  • Get a checked exception out of a lambda without losing it, and spot a catch (Exception e) that hides a bug.

Errors as values or exceptions showed that a Java failure usually travels as an exception, and that javac checks some exceptions and not others. This lesson shows which ones, and why. C#, Kotlin, Python, and TypeScript have no such check: any call may throw anything. In Java, the signature of a method lists some of the failures it can cause, and javac makes every caller react to them. That helps you see failures you would miss, and it gets in the way where a signature cannot list them, such as in a lambda. The examples follow a weightlifting competition: lifters are weighed, and their attempts are typed in.

Already know this? The short version
  • Error and RuntimeException, with their subclasses, are unchecked, and every other exception, such as IOException or a class you derive from Exception, is checked.
  • A call that can throw a checked exception must sit in a try that catches it or in a method whose throws clause declares it (error: unreported exception X; must be caught or declared to be thrown); a catch for a checked exception that nothing in the try can throw is refused (error: exception X is never thrown in body of corresponding try statement).
  • Extend Exception when callers can be expected to handle the failure, RuntimeException for a mistake in the calling code; give the class a (String message, Throwable cause) constructor. Catch blocks are tried top to bottom, a superclass above a subclass is refused (has already been caught), and catch (A | B e) handles unrelated classes in one block.
  • Function, map, and the other standard functional interfaces declare no checked exceptions: inside a lambda, wrap with new UncheckedIOException(e) or your own RuntimeException and unwrap with getCause() outside, or use a loop. The section “What goes wrong with exceptions in real programs?” covers swallowing, catch (Exception e) around one checked call, causes lost in a message, and throws Exception.

Go to the final check· Go to the practice tickets

Words in this lesson (8)
exception
An object that is thrown to stop the normal flow when an operation fails, such as the NumberFormatException from Integer.parseInt. A catch block can handle it.
Throwable
The class at the top of everything Java can throw. Its two standard subclasses are Error, for problems of the JVM itself, and Exception, for failures a program may handle.
checked exception
An exception that the compiler makes code handle: a method that can throw one must catch it or declare it with throws. Exception and its subclasses are checked, except RuntimeException and its subclasses.
unchecked exception
An exception of class RuntimeException or Error, or one of their subclasses, such as IllegalStateException. The compiler does not make code catch it or declare it.
throws clause
The part of a method’s signature that lists the checked exceptions it can pass to its caller, such as throws IOException in static int weigh(String lifter) throws IOException.
cause
The original exception kept inside a new one, as in new IllegalStateException("back hedge failed", problem). getCause() gives it back, and a stack trace prints it after Caused by:.
multi-catch
One catch block for several exception classes, written with |, such as catch (IOException | NumberFormatException e). The classes must not be subclasses of each other.
functional interface
An interface with exactly one abstract method, such as IntUnaryOperator with applyAsInt. A lambda or a method reference can be used wherever one is expected.

Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.

01 / Throwable

What can a Java program throw?

Everything a Java program can throw is an object of class Throwable or one of its subclasses. Throwable has two standard subclasses. Error is for problems of the JVM itself, such as StackOverflowError or OutOfMemoryError; a program usually cannot recover from them and does not catch them. Exception is for failures a program may handle, such as a file that cannot be read.

One subclass of Exception, RuntimeException, is special. Its subclasses usually mean a mistake in the calling code: IllegalArgumentException for a bad argument, NumberFormatException (a subclass of it) for text that is not a number, NullPointerException for a use of null.

This tree decides everything in the lesson. An exception is unchecked when its class is RuntimeException or Error, or has one of them among its superclasses. Every other exception, such as IOException, is checked. To find out, look at the class’s superclasses in its documentation, or ask the class itself, as the experiment does: getClass() returns the exception’s Class object, as in Java generic erasure and runtime types, and getSuperclass() returns the class above it.

Main.java
Throwable
├── Error                    unchecked
│   └── StackOverflowError
└── Exception                checked
    ├── IOException          checked
    └── RuntimeException     unchecked
        └── IllegalArgumentException
            └── NumberFormatException

An exception is unchecked when RuntimeException or Error is among its superclasses; every other exception is checked.

Coming from Python: Python’s BaseException and Exception look similar, but no Python exception is checked; Java’s split between Exception and RuntimeException is what the compiler reads.

Precisely: the exact rule

The Java Language Specification (11.1.1) defines the unchecked exception classes as RuntimeException, Error, and their subclasses; all other subclasses of Throwable are checked, including Throwable itself and Exception. The check is made only by javac. The JVM does not check throws clauses, so code compiled from another JVM language, or built with tricks around javac, can throw a checked exception from a method that does not declare it.

Experiment

Walk up the classes of two exceptions

Every class has one superclass, and getSuperclass() returns it. Object has none, so the loop stops there. Whether an exception is checked depends only on this chain: an exception with RuntimeException or Error in its chain is unchecked.

Why it matters To know whether javac will make you handle an exception, look at its superclasses: under RuntimeException or Error, it is unchecked; otherwise it is checked.

Predict both printed lines, and which of them contains RuntimeException. Then add the line showChain(new StackOverflowError()); at the end of main and predict a third line.

import java.io.IOException;

class Main {
    static void showChain(Throwable problem) {
        Class<?> kind = problem.getClass();
        String chain = kind.getSimpleName();
        while (kind.getSuperclass() != null) {
            kind = kind.getSuperclass();
            chain = chain + " < " + kind.getSimpleName();
        }
        System.out.println(chain);
    }

    public static void main(String[] args) {
        showChain(new NumberFormatException("120kg"));
        showChain(new IOException("scale offline"));
    }
}
What to look for

Step into showChain for the first call. problem shows java.lang.NumberFormatException@ and an id, with the exception’s internal fields. backtrace is where the exception was created, in the JVM’s own form. detailMessage holds the message, "120kg". cause shows the same exception again, with the same id: that is how a Throwable records that no cause was set, and getCause() returns null for it. kind shows java.lang.Class@ and an id, with name: null at first; Java fills in the name the first time it is used. Each pass of the loop moves kind one class up, and chain grows. The first line contains RuntimeException. For the IOException, the chain goes from IOException straight to Exception, so the second line has no RuntimeException. After the edit, a third line prints StackOverflowError < VirtualMachineError < Error < Throwable < Object.

Expected output

NumberFormatException < IllegalArgumentException < RuntimeException < Exception < Throwable < Object
IOException < Exception < Throwable < Object

02 / throws

Which exceptions must a method declare?

A method that can throw a checked exception must say so. throws IOException after the parameters is a throws clause: it is part of the method’s signature, next to its parameters and return type. javac then checks every call to that method. The call must be inside a try with a catch for IOException, or for one of its superclasses, or inside a method that declares IOException too.

If neither is true, javac stops with:

error: unreported exception IOException; must be caught or declared to be thrown

The rule also works the other way. javac knows from the throws clauses which checked exceptions the code in a try can throw. A catch for a checked exception that none of it can throw is refused:

error: exception IOException is never thrown in body of corresponding try statement

Neither rule applies to unchecked exceptions. Any method may throw IllegalArgumentException without a throws clause, and any try may catch it. catch (Exception e) is always allowed too, because Exception includes RuntimeException.

Main.java
static int weigh(String lifter) throws IOException {
    if (lifter.equals("Ken")) {
        throw new IOException("scale offline");
    }
    return 81;
}

// enter passes the IOException on to its caller
static String enter(String lifter) throws IOException {
    int kg = weigh(lifter);
    return lifter + " weighs " + kg + " kg";
}

A call that can throw a checked exception must be caught or declared; the throws clause passes the exception on to the caller.

Coming from C#: A C# method never lists its exceptions, and the compiler never asks; in Java, deleting one throws clause makes javac report the code inside the method and the catch above it.

Precisely: overriding and constructors

A method that overrides another may declare fewer checked exceptions, or subclasses of them, but not new ones (JLS 11.2.3 and 8.4.8.3); otherwise code that calls it through the parent class would not be warned. A constructor may have a throws clause, and so may main: public static void main(String[] args) throws IOException compiles, and an IOException that reaches the top stops the program with its stack trace.

Experiment

Pass a checked exception through a throws clause

throws IOException is part of a method’s signature, and javac checks it at every call. A call to a method that declares a checked exception must be in a try that catches it, or in a method that declares it too. javac also uses the clauses the other way: after the edit, nothing called in main’s try declares IOException, so a catch for it could never run, and javac refuses it.

Why it matters A throws clause passes a checked exception on to the caller. Remove it from one method, and javac reports the code inside it and the catch above it.

Predict both printed lines. Then delete throws IOException from the line static String enter(String lifter) throws IOException { and predict what javac says.

import java.io.IOException;

class Main {
    static int weigh(String lifter) throws IOException {
        if (lifter.equals("Ken")) {
            throw new IOException("scale offline");
        }
        return 81;
    }

    static String enter(String lifter) throws IOException {
        int kg = weigh(lifter);
        return lifter + " weighs " + kg + " kg";
    }

    public static void main(String[] args) {
        try {
            System.out.println(enter("Ana"));
            System.out.println(enter("Ken"));
        } catch (IOException problem) {
            System.out.println("not entered: " + problem.getMessage());
        }
    }
}
What to look for

Step into enter("Ken") and then into weigh. The frame list shows Main.weigh on top of Main.enter and Main.main. weigh stops on the throw line, and the next stop is the catch line in main: the return line in enter never runs for Ken. At the next stop, problem shows java.io.IOException@ and an id. The run prints Ana weighs 81 kg, then not entered: scale offline. After the edit, nothing runs, and javac reports two errors. The first is Main.java:12: error: unreported exception IOException; must be caught or declared to be thrown, under the call weigh(lifter). The second is Main.java:20: error: exception IOException is never thrown in body of corresponding try statement, under the catch in main.

Expected output

Ana weighs 81 kg
not entered: scale offline

03 / Your own class

Should your own exception be checked or unchecked?

Your own exception class extends Exception or RuntimeException, and that choice decides whether it is checked.

Extend Exception when the caller can be expected to handle the failure, and you want javac to remind every caller: a scale that is offline, a file that is missing, a line someone typed wrong. Extend RuntimeException when the failure means the calling code made a mistake, such as passing a negative weight; the caller should fix its code, not catch the exception. For a bad argument, the standard IllegalArgumentException is often enough. Never extend Error or Throwable directly.

Give the class a constructor that takes a message and a cause. super(message, cause) in a constructor runs the constructor of the parent class, Exception here, which stores both. Without that constructor, code that wraps another exception has to drop it, and getCause() returns null. Fields can carry details, such as which lifter or which weight, so the catching code does not have to read them out of the message.

Main.java
class AttemptException extends Exception {
    final int kg;

    AttemptException(String message, int kg, Throwable cause) {
        super(message, cause);
        this.kg = kg;
    }
}

Extend Exception for a failure callers should handle, RuntimeException for a mistake in the calling code, and pass the cause to super.

Coming from Kotlin: Kotlin ignores checked exceptions; when Kotlin code calls a Java method that throws IOException, nothing makes it catch one, and when Java calls Kotlin, @Throws must add the clause.

Precisely: what other libraries choose

The choice is debated. Many newer Java libraries throw only unchecked exceptions, because a checked exception in a widely used signature forces every caller to write a try or a throws clause, even callers that can do nothing. The Java tutorial’s rule is the one above: if a caller can reasonably be expected to recover, make it checked.

04 / Catch order

Which catch block receives the exception?

A try can have several catch blocks. When an exception is thrown in the try, Java tries the catch blocks from top to bottom and runs only the first one whose class matches: the exception’s own class or one of its superclasses. The other catch blocks are skipped.

So the order matters when one class is a subclass of another. NumberFormatException is a subclass of IllegalArgumentException. If the IllegalArgumentException block came first, it would receive both, and a NumberFormatException block below it could never run. javac refuses such code:

error: exception NumberFormatException has already been caught

Put the narrower class first.

When two unrelated classes need the same handling, a multi-catch writes one block for both: catch (IOException | NumberFormatException problem). The classes in it must not be subclasses of each other; for catch (NumberFormatException | IllegalArgumentException problem), javac reports error: Alternatives in a multi-catch statement cannot be related by subclassing.

Main.java
try {
    return "lift " + attempt(typed);
} catch (NumberFormatException problem) {
    // the narrower class first
    return "not a number: " + typed;
} catch (IllegalArgumentException problem) {
    return "refused: " + problem.getMessage();
}

The first catch block whose class matches runs; put subclasses above their superclasses, and use | for unrelated classes with the same handling.

Precisely: the type in a multi-catch

In catch (A | B e), e is implicitly final, and its type for javac is the nearest common superclass of A and B. A catch block that rethrows its exception with throw e, where e was not reassigned, rethrows only the checked classes the try can actually throw, so the method’s throws clause may name those classes instead of Exception (JLS 11.2.2).

Experiment

Send two exceptions to two catch blocks

Java tries the catch blocks from top to bottom and runs the first one whose class is the exception’s class or one of its superclasses. Every NumberFormatException is also an IllegalArgumentException, so after the swap the first block would receive both, and the second block could never run. javac refuses a catch block that can never run.

Why it matters Put the narrower catch first. A catch for a superclass placed above it makes it unreachable, and javac says so.

NumberFormatException is a subclass of IllegalArgumentException. Predict the three printed lines. Then swap the two catch blocks, so that the IllegalArgumentException block comes first, and predict again.

class Main {
    static int attempt(String typed) {
        int kg = Integer.parseInt(typed);
        if (kg > 200) {
            throw new IllegalArgumentException("over 200 kg: " + kg);
        }
        return kg;
    }

    static String record(String typed) {
        try {
            return "lift " + attempt(typed);
        } catch (NumberFormatException problem) {
            return "not a number: " + typed;
        } catch (IllegalArgumentException problem) {
            return "refused: " + problem.getMessage();
        }
    }

    public static void main(String[] args) {
        System.out.println(record("120"));
        System.out.println(record("120kg"));
        System.out.println(record("250"));
    }
}
What to look for

For "120kg", the stop on the parseInt line in attempt is followed by the first catch line in record: a NumberFormatException matches the first catch. For "250", attempt stops on its throw line, and the next stop is the second catch line: an IllegalArgumentException is not a NumberFormatException, so the first catch is skipped. Inside each catch block, problem shows the exception’s class and id. The run prints lift 120, then not a number: 120kg, then refused: over 200 kg: 250. After the edit, nothing runs, and javac reports Main.java:15: error: exception NumberFormatException has already been caught.

Expected output

lift 120
not a number: 120kg
refused: over 200 kg: 250

05 / Lambdas

Why can a lambda not throw a checked exception?

A lambda is the body of one method of a functional interface. map takes a Function, and Function’s method is R apply(T t), with no throws clause. So a lambda passed to map may not throw a checked exception, even when the method around it declares one. map(lifter -> weigh(lifter)) gives error: unreported exception IOException; must be caught or declared to be thrown, under the call to weigh.

The quick fix is to catch the exception inside the lambda and return a value. That compiles, and it is wrong: the failure becomes a number, such as 0, and nothing tells the code that called map. The experiment below shows a team weight that is too small, with no error.

There are three honest fixes.

  • Use a for loop. The loop body is part of your method, so the method’s own throws clause covers it.
  • Catch inside the lambda and throw an unchecked exception with the checked one as its cause. Java has UncheckedIOException for an IOException; for other classes, write a RuntimeException subclass. Outside the stream, catch it and throw getCause() again, so callers see the checked exception they expect.
  • Declare your own functional interface whose method has a throws clause. It works for your own methods, but not with map, which accepts only Function.
Main.java
static int teamWeight(List<String> team) throws IOException {
    try {
        return team.stream()
                .map(lifter -> {
                    try {
                        return weigh(lifter);
                    } catch (IOException problem) {
                        throw new UncheckedIOException(problem);
                    }
                })
                .reduce(0, (sum, kg) -> sum + kg);
    } catch (UncheckedIOException problem) {
        // getCause() is the original IOException
        throw problem.getCause();
    }
}

A lambda passed to map cannot throw a checked exception; use a loop, or wrap the exception in an unchecked one with a cause and unwrap it outside.

Coming from C#: A C# lambda in a LINQ Select may throw anything; the same Java code does not compile until the lambda handles the checked exception.

Precisely: why the cast is not needed here

UncheckedIOException overrides getCause() to return IOException, so throw problem.getCause() matches throws IOException. For your own wrapper, getCause() returns Throwable, and you write throw (ScaleException) problem.getCause(), a cast that is safe only if every place that makes the wrapper passes that class. Some libraries rethrow a checked exception without wrapping it, through a generic trick that makes javac believe it is unchecked; the throws clauses then no longer tell the truth, and a catch for it may be refused as never thrown.

Experiment

Hide a failed weighing inside a lambda

map takes a Function, and Function’s apply method declares no checked exception, so the lambda must handle the IOException itself. return 0 handles it by hiding it. After the edit, the lambda throws an unchecked exception that keeps the IOException as its cause. It passes through the stream code, which declares nothing, and main catches it and reads getCause().

Why it matters Inside a lambda, turn a checked exception into an unchecked one that keeps it as the cause, never into a default value.

Predict the printed line. Then replace return 0; in the lambda with throw new UncheckedIOException(problem); and predict again.

import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.List;

class Main {
    static int weigh(String lifter) throws IOException {
        if (lifter.equals("Ken")) {
            throw new IOException("scale offline for Ken");
        }
        return 80;
    }

    public static void main(String[] args) {
        List<String> team = List.of("Ana", "Ken", "Ola");
        try {
            int total = team.stream()
                    .map(lifter -> {
                        try {
                            return weigh(lifter);
                        } catch (IOException problem) {
                            return 0;
                        }
                    })
                    .reduce(0, (sum, kg) -> sum + kg);
            System.out.println("team weight " + total);
        } catch (UncheckedIOException problem) {
            System.out.println("stopped: " + problem.getCause().getMessage());
        }
    }
}
What to look for

The stepper stops on the stream lines and then on the reduce line before any lambda runs: the lambdas run inside reduce. A frame named Main.lambda in main appears with lifter "Ana", and weigh returns 80. Then the lambda that adds runs, with sum and kg each shown as java.lang.Integer@… {value: …}. For Ken, weigh throws, and the next stop is the catch line inside the lambda, which then returns 0. Nothing tells main: total shows 160, and the run prints team weight 160, as if Ken weighed nothing. After the edit, the stop after the lambda’s catch line is the catch line in main. problem shows java.io.UncheckedIOException@ and an id, and the run prints stopped: scale offline for Ken.

Expected output

team weight 160

06 / In production

What goes wrong with exceptions in real programs?

Each mistake below compiles, and most were written only to stop a javac error.

Swallowing. catch (IOException e) {} with an empty block, or a block that only returns a default value, ends the failure there. Nothing is printed, and the code above continues with a wrong value. If a method cannot handle an exception, declare it or wrap it; at the least, wrap it in an unchecked exception with the cause.

A catch (Exception e) around one checked call. It is shorter than naming the class, and it also receives every unchecked exception from the same try: a NullPointerException from a missing map entry, as in Java final, primitives, and boxing, gets the message written for the checked one. Catch the class the call declares.

A cause lost in a message. throw new AttemptException("refused: " + e.getMessage()) keeps the text and loses the class and the stack trace of e. Pass e as the cause, which needs the constructor from the section on your own class.

throws Exception on every method. It removes every javac error about checked exceptions, and it forces every caller to catch Exception, which brings back the second mistake.

Main.java
// the second mistake: only an IOException was expected
try {
    int made = attemptsMade(lifter);
    int kg = plans.get(lifter); // null for Ola
    return lifter + ", attempt " + (made + 1) + ": " + kg + " kg";
} catch (Exception problem) {
    return lifter + ": score card not read";
}

Do not swallow, catch the class a call declares rather than Exception, pass the cause, and declare the checked classes you throw rather than Exception.

Experiment

Catch a bug with a catch meant for a score card

catch (Exception problem) receives every exception under Exception, checked or unchecked. It was written because attemptsMade declares an IOException, but it also receives the NullPointerException from a missing entry, a bug in the data, and prints the score card message for it.

Why it matters Catch the checked exception a call declares, not Exception. A bug should stop the program with its own message, not borrow another one.

Predict both printed lines. Then change catch (Exception problem) to catch (IOException problem) and predict again.

import java.io.IOException;
import java.util.Map;

class Main {
    static int attemptsMade(String lifter) throws IOException {
        if (lifter.isEmpty()) {
            throw new IOException("no score card");
        }
        return 2;
    }

    static String next(Map<String, Integer> plans, String lifter) {
        try {
            int made = attemptsMade(lifter);
            int kg = plans.get(lifter);
            return lifter + ", attempt " + (made + 1) + ": " + kg + " kg";
        } catch (Exception problem) {
            return lifter + ": score card not read";
        }
    }

    public static void main(String[] args) {
        Map<String, Integer> plans = Map.of("Ana", 95);
        System.out.println(next(plans, "Ana"));
        System.out.println(next(plans, "Ola"));
    }
}
What to look for

Step into next for Ola. attemptsMade returns 2, and made shows 2. The next stop after int kg = plans.get(lifter); is the catch line. plans has no entry for Ola, so get returns null, and turning null into an int throws a NullPointerException. At the stop inside the catch block, problem shows java.lang.NullPointerException@ and an id, not an IOException. The run prints Ana, attempt 3: 95 kg, then Ola: score card not read, which is false: the card was read. After the edit, nothing catches the NullPointerException. The run stops after the first line, with java.lang.NullPointerException.

Expected output

Ana, attempt 3: 95 kg
Ola: score card not read

07 / Try it

Which catch block runs for each attempt?

A typed attempt becomes the kilograms on each side of a 20 kg bar. Decide which catch block in main receives each failure.

Main.java
class Main {
    static int perSide(String typed) throws AttemptException {
        try {
            int kg = Integer.parseInt(typed);
            if (kg > 200) {
                throw new IllegalStateException("over 200 kg");
            }
            return (kg - 20) / 2;
        } catch (NumberFormatException problem) {
            throw new AttemptException("cannot read " + typed, problem);
        }
    }

    public static void main(String[] args) {
        String[] typed = {"60", "sixty", "250"};
        for (String one : typed) {
            try {
                System.out.println(one + ": " + perSide(one) + " kg a side");
            } catch (AttemptException problem) {
                Throwable cause = problem.getCause();
                System.out.println(problem.getMessage() + ", cause "
                        + cause.getClass().getSimpleName());
            } catch (RuntimeException problem) {
                System.out.println("refused: " + problem.getMessage());
            }
        }
    }
}

class AttemptException extends Exception {
    AttemptException(String message, Throwable cause) {
        super(message, cause);
    }
}

What are the three printed lines?

Reveal the reasoning

60: 20 kg a side, then cannot read sixty, cause NumberFormatException, then refused: over 200 kg. For "sixty", parseInt throws a NumberFormatException, the catch in perSide wraps it in the checked AttemptException with the original as the cause, and the first catch in main receives it. For "250", the IllegalStateException is thrown inside the same try in perSide, but that catch accepts only NumberFormatException, so the exception leaves perSide unchanged. It is not an AttemptException, so main’s first catch is skipped, and the RuntimeException catch receives it. perSide needs no throws clause for it, because it is unchecked.

Practice tickets

  1. JAVA-701 · The booking desk cannot say why a court was refused → Free
  2. JAVA-702 · A skier with a mistyped run gets the fastest combined time → Sign in · Pro
  3. JAVA-703 · A new club’s results disappear into the skipped count → Sign in · Pro

Keep exploring

I/O, buffering, and external state →

Primary references