What an exception is

Java reports problems that occur while a program runs with exceptions: an array index that does not exist, text that is not a number, a missing file, a method called on null. When an operation cannot be carried out, Java creates an exception object and throws it. The current statement stops, and the Java Virtual Machine (JVM) looks for code that handles that type of exception, first in the current method, then in the method that called it, and so on. If it finds none, the thread ends — for the main thread, that means the program ends with exit code 1.

public class Main {
    public static void main(String[] args) {
        String[] tickets = {"INC-10231", "INC-10232"};

        System.out.println("First ticket: " + tickets[0]);
        System.out.println("Third ticket: " + tickets[2]);
        System.out.println("Done");                    // never reached
    }
}
First ticket: INC-10231
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 2 out of bounds for length 2

Mistakes the compiler finds — a missing semicolon, a String assigned to an int — are not exceptions: the program is never compiled, so it never runs. Java's compiler is strict, and on this page you will see it refuse several programs that would run (and fail) in Python or JavaScript.

Reading a stack trace

Suppose MeterReport.java adds up meter readings and one of them contains a typo. Compiled with javac and run with java, it stops with this report on the standard error stream:

public class MeterReport {
    public static void main(String[] args) {
        String[] readings = {"412", "415", "41x9"};
        System.out.println("Total: " + total(readings) + " kWh");
    }

    static int total(String[] readings) {
        int sum = 0;
        for (String reading : readings) {
            sum += parse(reading);
        }
        return sum;
    }

    static int parse(String reading) {
        return Integer.parseInt(reading);
    }
}
Exception in thread "main" java.lang.NumberFormatException: For input string: "41x9"
	at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
	at java.base/java.lang.Integer.parseInt(Integer.java:565)
	at java.base/java.lang.Integer.parseInt(Integer.java:662)
	at MeterReport.parse(MeterReport.java:16)
	at MeterReport.total(MeterReport.java:10)
	at MeterReport.main(MeterReport.java:4)

The first line gives the thread, the full class name of the exception and its message. The lines starting with at are the stack trace: the chain of method calls that was active when the exception was thrown, most recent call first (the opposite of a Python traceback). Read it like this:

  1. The top three frames are inside the Java Development Kit (JDK). java.base/ is the module the class lives in; the line numbers refer to the JDK's own source and change between updates. Skip down to the first frame of your own code.
  2. MeterReport.parse, line 16, is where your code called parseInt with the bad text.
  3. Line 10 in total called parse, and line 4 in main called total.

The failing line is rarely where the mistake is: here the data is wrong, and the program has no plan for bad data. The rest of this page is about making that plan.

try, catch and finally

Put the code that may fail in a try block. Each catch clause names an exception type and a variable; it runs only when an exception of that type (or a subclass) is thrown inside the try. The rest of the try block is skipped, at most one catch runs, and the program continues below the whole statement.

public class Main {
    public static void main(String[] args) {
        String[] lines = {"420;30", "12.5;30", "420;0"};

        for (String line : lines) {
            String[] parts = line.split(";");
            try {
                int kwh = Integer.parseInt(parts[0]);
                int days = Integer.parseInt(parts[1]);
                System.out.println("Average: " + kwh / days + " kWh per day");
            } catch (NumberFormatException e) {
                System.out.println("Not a whole number in: " + line);
            } catch (ArithmeticException e) {
                System.out.println("Zero days in: " + line);
            }
        }
        System.out.println("Program continues");
    }
}
Average: 14 kWh per day
Not a whole number in: 12.5;30
Zero days in: 420;0
Program continues

Integer division by zero throws an ArithmeticException; division of double values does not (it gives Infinity), as explained in Variables, Types and Operators.

The exception object

The variable in the catch clause (by convention e) is the exception object. getMessage() returns its message, getClass() its type, and toString() — used when you print it — combines the two.

public class Main {
    public static void main(String[] args) {
        try {
            int kwh = Integer.parseInt("12x4");
            System.out.println(kwh);
        } catch (NumberFormatException e) {
            System.out.println(e.getClass().getName());
            System.out.println(e.getClass().getSimpleName());
            System.out.println(e.getMessage());
            System.out.println(e);
        }
    }
}
java.lang.NumberFormatException
NumberFormatException
For input string: "12x4"
java.lang.NumberFormatException: For input string: "12x4"

One handler for several types: multi-catch

When several exception types need the same handling, list them in one catch separated by |. The variable then has the most specific common supertype.

public class Main {
    public static void main(String[] args) {
        String[] lines = {"420;30", "12.5;30", "420;0"};

        for (String line : lines) {
            String[] parts = line.split(";");
            try {
                int average = Integer.parseInt(parts[0]) / Integer.parseInt(parts[1]);
                System.out.println(line + " -> " + average + " kWh per day");
            } catch (NumberFormatException | ArithmeticException e) {
                System.out.println(line + " -> skipped (" + e.getClass().getSimpleName() + ")");
            }
        }
    }
}
420;30 -> 14 kWh per day
12.5;30 -> skipped (NumberFormatException)
420;0 -> skipped (ArithmeticException)

Order matters: specific first

A catch for a superclass also catches all its subclasses, so a general clause above a specific one would make the specific one unreachable. The compiler refuses that order:

public class Main {
    public static void main(String[] args) {
        try {
            System.out.println(Integer.parseInt("twelve"));
        } catch (Exception e) {
            System.out.println("Something went wrong");
        } catch (NumberFormatException e) {
            System.out.println("Not a number");
        }
    }
}
Main.java:7: error: exception NumberFormatException has already been caught

finally: clean-up that always runs

A finally block runs after the try block in every normal case: when it succeeds, when an exception is caught, when an exception passes through uncaught, and when the block is left with return. (A JVM that is killed or crashes, or a call to System.exit, skips it.) A plain try needs at least one catch or a finally (a try-with-resources needs neither).

public class Main {
    public static void main(String[] args) {
        System.out.println(readGateway(true));

        try {
            readGateway(false);
        } catch (IllegalStateException e) {
            System.out.println("Failed: " + e.getMessage());
        }
    }

    static String readGateway(boolean online) {
        try {
            System.out.println("Opening connection to meter gateway");
            if (!online) {
                throw new IllegalStateException("Meter gateway is offline");
            }
            return "Reading received";
        } finally {
            System.out.println("Closing connection");
        }
    }
}
Opening connection to meter gateway
Closing connection
Reading received
Opening connection to meter gateway
Closing connection
Failed: Meter gateway is offline

"Closing connection" appears before "Reading received": the return value is ready, but finally runs on the way out of the method, before the caller prints it.

The exception hierarchy

Everything that can be thrown is an object of the class Throwable (package java.lang) or one of its subclasses. It carries the message, the stack trace, an optional cause and any suppressed exceptions. Directly below it are two branches with a different purpose:

  • Error: serious problems a reasonable application should not try to catch, such as running out of memory (OutOfMemoryError) or runaway recursion (StackOverflowError). Applications should not try to handle them; see Running Java in Production.
  • Exception: conditions a program may reasonably want to handle. Its subclass RuntimeException covers programming errors and invalid arguments.
The Throwable hierarchy. Throwable has two subclasses, Error and Exception. Error, with OutOfMemoryError and StackOverflowError, is unchecked. Exception has the subclass RuntimeException, with NullPointerException, IllegalArgumentException and IllegalStateException, which is unchecked; IOException, with NoSuchFileException, and the other subclasses of Exception are checked. Throwable Error unchecked OutOfMemoryError StackOverflowError JVM problems: do not catch Exception RuntimeException unchecked NullPointerException IllegalArgumentException IllegalStateException IOException checked NoSuchFileException (and every other Exception subclass)
Part of the hierarchy. Everything under RuntimeException or Error is unchecked; every other exception is checked by the compiler.

Catching a class catches its whole branch. The program below sorts a few exceptions into the three groups with instanceof:

import java.io.IOException;
import java.nio.file.NoSuchFileException;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Throwable> problems = List.of(
                new NumberFormatException("bad reading"),
                new IllegalStateException("gateway offline"),
                new NoSuchFileException("tariffs.csv"),
                new IOException("disk full"),
                new StackOverflowError());

        for (Throwable t : problems) {
            String kind;
            if (t instanceof Error) {
                kind = "Error (do not catch)";
            } else if (t instanceof RuntimeException) {
                kind = "unchecked";
            } else {
                kind = "checked";
            }
            System.out.printf("%-26s %s%n", t.getClass().getSimpleName(), kind);
        }
    }
}
NumberFormatException      unchecked
IllegalStateException      unchecked
NoSuchFileException        checked
IOException                checked
StackOverflowError         Error (do not catch)

The exceptions you will meet most often:

Exception Typical cause
NullPointerException Using a variable that is null — see Null, Optional and NullPointerException
IllegalArgumentException A method received an argument it cannot accept; NumberFormatException is a subclass
IllegalStateException The object is in the wrong state for this call
ArrayIndexOutOfBoundsException, IndexOutOfBoundsException An index outside an array, list or string
ClassCastException A cast to a type the object does not have
UnsupportedOperationException Changing an unmodifiable collection such as List.of(…)
IOException, NoSuchFileException (checked) Files, folders and network connections — see Files, I/O and JSON

Checked and unchecked exceptions

Unlike Python, JavaScript and C#, Java has a compile-time rule for exceptions. A checked exception is one the compiler makes you deal with: every subclass of Exception that is not a subclass of RuntimeException. If code can throw one, the method must either catch it or declare it. The typical checked exceptions describe things outside the program's control that a careful program should expect: a missing file, a dropped network connection, a database that does not answer.

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class Main {
    public static void main(String[] args) {
        String tariffs = readTariffs();
        System.out.println(tariffs);
    }

    static String readTariffs() {
        return Files.readString(Path.of("tariffs.csv"));
    }
}
Main.java:12: error: unreported exception IOException; must be caught or declared to be thrown

An unchecked exception is any subclass of RuntimeException or Error. The compiler does not track them, because they can happen almost anywhere and usually signal a bug that should be fixed rather than handled: a null where an object was expected, a wrong index, an invalid argument.

Declaring with throws

A method that does not want to handle a checked exception itself passes it on to its caller with a throws clause after the parameter list. The clause is part of the method's contract: every caller now has the same choice — catch it, or declare it too.

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;

public class Main {
    public static void main(String[] args) {
        try {
            System.out.println(readTariffs());
        } catch (NoSuchFileException e) {
            System.out.println("Missing file: " + e.getMessage());
        } catch (IOException e) {
            System.out.println("Could not read tariffs: " + e);
        }
    }

    static String readTariffs() throws IOException {
        return Files.readString(Path.of("tariffs.csv"));
    }
}
Missing file: tariffs.csv

The file does not exist in the folder where the example runs, so Files.readString throws NoSuchFileException, a subclass of IOException. main may also declare throws IOException; then an uncaught exception simply ends the program with a stack trace. That is fine for small tools, not for a service that should keep running.

Throwing exceptions

Your own methods signal errors with throw new SomeException("message"). Throw when a method cannot do what its name promises, and prefer an existing type that callers already know:

  • IllegalArgumentException: the caller passed a bad argument. For a null argument, Objects.requireNonNull(value, "message") throws a NullPointerException with your message.
  • IllegalStateException: the arguments are fine, but the object is not in a state that allows this call.
  • UnsupportedOperationException: the operation is not available at all.

Checks at the top of a method that throw on bad input are called guard clauses. They make a method fail fast, at the place where the bad value enters, instead of somewhere far away later:

import java.math.BigDecimal;

public class Main {
    public static void main(String[] args) {
        System.out.println(cost(120, new BigDecimal("0.32")));
        System.out.println(cost(-5, new BigDecimal("0.32")));
    }

    static BigDecimal cost(int kwh, BigDecimal tariff) {
        if (kwh < 0) {
            throw new IllegalArgumentException("kWh must not be negative: " + kwh);
        }
        if (tariff.signum() <= 0) {
            throw new IllegalArgumentException("tariff must be positive: " + tariff);
        }
        return tariff.multiply(BigDecimal.valueOf(kwh));
    }
}
38.40
Exception in thread "main" java.lang.IllegalArgumentException: kWh must not be negative: -5

Rethrowing

Sometimes a method wants to react to an exception — log it, undo something — without handling it. Inside the catch, throw e; sends the same exception on. Its stack trace is not changed: in Java the trace is recorded when the exception object is created, not when it is thrown.

public class Main {
    public static void main(String[] args) {
        try {
            parseReading("forty");
        } catch (NumberFormatException e) {
            for (StackTraceElement frame : e.getStackTrace()) {
                if (frame.getClassName().equals("Main")) {
                    System.out.println("First frame in our code: " + frame.getMethodName()
                            + ", line " + frame.getLineNumber());
                    break;
                }
            }
        }
    }

    static int parseReading(String text) {
        try {
            return Integer.parseInt(text);
        } catch (NumberFormatException e) {
            System.out.println("log: could not parse '" + text + "'");
            throw e;
        }
    }
}
log: could not parse 'forty'
First frame in our code: parseReading, line 18

Line 18 is the parseInt call, not the throw e; on line 21. (In C#, throw e; resets the trace and you write throw; instead; Java has no bare throw;.)

Custom exceptions and chaining

When no built-in type describes an error well and callers should be able to catch exactly that error, define your own class. Extend Exception for a checked exception or RuntimeException for an unchecked one; end the name in Exception; and offer a constructor that takes a cause.

Exception chaining means wrapping a low-level exception inside a higher-level one, passing the original as the cause: super(message, cause). The caller sees an exception that fits its level ("this meter reading is invalid") and can still get the technical detail with getCause().

public class Main {
    public static void main(String[] args) {
        String[] lines = {"E-001;412", "E-002;41x9"};

        for (String line : lines) {
            try {
                System.out.println(line + " -> " + parseReading(line) + " kWh");
            } catch (MeterReadingException e) {
                System.out.println(e.getMessage());
                System.out.println("  meter:     " + e.getMeterId());
                System.out.println("  caused by: " + e.getCause());
            }
        }
    }

    static int parseReading(String line) throws MeterReadingException {
        String[] parts = line.split(";");
        try {
            return Integer.parseInt(parts[1]);
        } catch (NumberFormatException e) {
            throw new MeterReadingException(parts[0], "Invalid reading for " + parts[0] + ": " + parts[1], e);
        }
    }
}

class MeterReadingException extends Exception {
    private final String meterId;

    MeterReadingException(String meterId, String message, Throwable cause) {
        super(message, cause);
        this.meterId = meterId;
    }

    String getMeterId() {
        return meterId;
    }
}
E-001;412 -> 412 kWh
Invalid reading for E-002: 41x9
  meter:     E-002
  caused by: java.lang.NumberFormatException: For input string: "41x9"

Classes, constructors and super(…) are explained in Classes and Objects and Inheritance, Interfaces and Polymorphism. When a chained exception is not caught, the stack trace shows the whole chain. From a compiled ImportReadings program with the same logic:

Exception in thread "main" MeterReadingException: Invalid reading for E-002
	at ImportReadings.parseReading(ImportReadings.java:11)
	at ImportReadings.main(ImportReadings.java:3)
Caused by: java.lang.NumberFormatException: For input string: "41x9"
	at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
	at java.base/java.lang.Integer.parseInt(Integer.java:565)
	at java.base/java.lang.Integer.parseInt(Integer.java:662)
	at ImportReadings.parseReading(ImportReadings.java:9)
	... 1 more

In logs from large applications the most useful line is often the last "Caused by:" — the original problem — while the top exception says what the application was trying to do. ... 1 more means the remaining frames are the same as those of the exception above, so Java leaves them out.

try-with-resources

Files, network sockets and database connections hold resources that the operating system hands out in limited numbers. The garbage collector frees memory, but not at a predictable moment, so these objects must be closed explicitly. In Java they implement the interface AutoCloseable, whose single method close() releases the resource.

try-with-resources (Java 7) is a try statement with a resource declaration in parentheses: try (var r = open()) { … }. Java calls close() on each resource when the block ends, normally or through an exception, in the reverse order of opening — before any catch or finally of the same statement runs. It is Java's counterpart of Python's with statement and C#'s using.

public class Main {
    public static void main(String[] args) {
        try (MeterConnection first = new MeterConnection("GW-07");
             MeterConnection second = new MeterConnection("GW-08")) {
            first.read();
            second.read();
            throw new IllegalStateException("No answer from meter 3");
        } catch (IllegalStateException e) {
            System.out.println("Caught: " + e.getMessage());
        }
    }
}

class MeterConnection implements AutoCloseable {
    private final String gateway;

    MeterConnection(String gateway) {
        this.gateway = gateway;
        System.out.println("Open " + gateway);
    }

    void read() {
        System.out.println("Reading from " + gateway);
    }

    @Override
    public void close() {
        System.out.println("Close " + gateway);
    }
}
Open GW-07
Open GW-08
Reading from GW-07
Reading from GW-08
Close GW-08
Close GW-07
Caught: No answer from meter 3

In real code you rarely write close() yourself; you put what the JDK gives you in the parentheses: Files.newBufferedReader(path), Files.lines(path), a JDBC Connection and PreparedStatement. Reading files this way is shown in Files, I/O and JSON.

Suppressed exceptions

What if the block throws, and then close() throws as well? With a hand-written finally, the second exception would replace the first, and the real cause would be lost. try-with-resources keeps the first exception and attaches the one from close() to it as a suppressed exception:

public class Main {
    public static void main(String[] args) {
        try (FlakyConnection connection = new FlakyConnection()) {
            throw new IllegalStateException("read failed");
        } catch (IllegalStateException e) {
            System.out.println("Caught: " + e.getMessage());
            for (Throwable suppressed : e.getSuppressed()) {
                System.out.println("Suppressed: " + suppressed.getMessage());
            }
        }
    }
}

class FlakyConnection implements AutoCloseable {
    @Override
    public void close() {
        throw new IllegalStateException("close failed");
    }
}
Caught: read failed
Suppressed: close failed

A printed stack trace lists them under a "Suppressed:" heading.

Good practices

Expected failures are not exceptional

Java has no TryParse method like C#: Integer.parseInt throws on bad text. When bad input is an ordinary outcome — a user typing "twelve" — handle it close to the source and turn it into a normal value, for example an empty OptionalInt (see Null, Optional and NullPointerException):

import java.util.OptionalInt;

public class Main {
    public static void main(String[] args) {
        for (String input : new String[] {"412", "twelve", " 415 ", ""}) {
            OptionalInt kwh = parseKwh(input);
            if (kwh.isPresent()) {
                System.out.println("Saved " + kwh.getAsInt() + " kWh");
            } else {
                System.out.println("'" + input + "' is not a whole number, please try again");
            }
        }
    }

    static OptionalInt parseKwh(String text) {
        try {
            return OptionalInt.of(Integer.parseInt(text.strip()));
        } catch (NumberFormatException e) {
            return OptionalInt.empty();
        }
    }
}
Saved 412 kWh
'twelve' is not a whole number, please try again
Saved 415 kWh
'' is not a whole number, please try again

Rules of thumb

  • Catch what you can handle. Catch a specific type where you know what to do about it — retry, ask again, use a default — and let everything else travel up.
  • Never swallow exceptions. An empty catch (Exception e) { } hides bugs and makes problems impossible to diagnose. If the compiler forces you to catch a checked exception you cannot handle, wrap it in an unchecked one with the cause — the JDK has UncheckedIOException for exactly that.
  • Catch broadly only at the edge — the top of a program, a request handler, a scheduled job — to log the full exception, including its stack trace and causes, and fail gracefully. Logging libraries do that when you pass the exception as the last argument; e.printStackTrace() writes to standard error and is fine for small tools only.
  • Do not catch Throwable or Error. After an OutOfMemoryError the JVM may not be in a state to continue; let it end and restart, as described in Running Java in Production.
  • Do not catch NullPointerException or IndexOutOfBoundsException. They signal a bug; fix the code instead.
  • Do not show stack traces to end users. They help attackers and confuse customers; log them and show a short, general message (see Web Application Security).