Exceptions and Error Handling
Handling errors in Java: try, catch and finally, checked versus unchecked exceptions, throws, try-with-resources, custom exceptions and reading a stack trace.
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:
-
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. -
MeterReport.parse, line 16, is where your code calledparseIntwith the bad text. -
Line 10 in
totalcalledparse, and line 4 inmaincalledtotal.
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 subclassRuntimeExceptioncovers programming errors and invalid arguments.
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 anullargument,Objects.requireNonNull(value, "message")throws aNullPointerExceptionwith 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 hasUncheckedIOExceptionfor 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
ThrowableorError. After anOutOfMemoryErrorthe JVM may not be in a state to continue; let it end and restart, as described in Running Java in Production. -
Do not catch
NullPointerExceptionorIndexOutOfBoundsException. 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).