Skip to main content
Java · lesson 18 of 66
Open the Java page →
Java beginner Lesson 18 of 66

Error Types in Java

Understand Java's error types — compile errors, checked and unchecked exceptions, and Errors — with real stack traces showing how to read each one.

By SkillByExample · Updated

Java programs fail in three different ways: the compiler rejects the code, an exception is thrown while it runs, or the JVM itself hits an Error it cannot recover from. Knowing which one you are looking at tells you where the fix goes. Every example is a single file run with the JDK 21 source launcher (java File.java).

The hierarchy

Everything that can be thrown extends Throwable:

Throwable
├── Error                         serious JVM problems — don't catch
│   ├── OutOfMemoryError
│   └── StackOverflowError
└── Exception
    ├── IOException               checked
    ├── SQLException              checked
    └── RuntimeException          unchecked
        ├── NullPointerException
        ├── ArithmeticException
        ├── ArrayIndexOutOfBoundsException
        ├── ClassCastException
        ├── IllegalArgumentException
        │   └── NumberFormatException
        └── IllegalStateException

Checked means the compiler forces you to deal with it. Everything under RuntimeException and Error is unchecked.

Compile errors happen before anything runs

public class CompileError {
    public static void main(String[] args) {
        System.out.println("this never prints");
        int count = "three";
    }
}
$ java CompileError.java
CompileError.java:4: error: incompatible types: String cannot be converted to int
        int count = "three";
                    ^
1 error
error: compilation failed

Even the first println didn’t run: the whole file must compile first. The message gives the file, the line (4), and a caret under the offending expression.

Checked exceptions are enforced by the compiler

Calling a method that declares a checked exception without catching or declaring it is itself a compile error.

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

public class Checked {
    public static void main(String[] args) {
        String text = Files.readString(Path.of("config.txt"));
        System.out.println(text);
    }
}
$ java Checked.java
Checked.java:6: error: unreported exception IOException; must be caught or declared to be thrown
        String text = Files.readString(Path.of("config.txt"));
                                      ^
1 error
error: compilation failed

Files.readString is declared throws IOException, so the compiler insists on a plan for a missing file. Catching it gives one:

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

public class CheckedHandled {
    public static void main(String[] args) {
        try {
            System.out.println(Files.readString(Path.of("config.txt")));
        } catch (IOException e) {
            System.out.println("Could not read: " + e);
        }
    }
}
$ java CheckedHandled.java
Could not read: java.nio.file.NoSuchFileException: config.txt

The exception that actually arrived is NoSuchFileException, a subclass of IOException, so the catch (IOException e) block matched it. The alternative is to add throws IOException to main and let the caller (here, the JVM) deal with it.

Unchecked exceptions: programming errors

The compiler doesn’t require you to handle RuntimeExceptions; they usually mean a bug. This program triggers six common ones and prints each:

import java.util.List;

public class Unchecked {
    public static void main(String[] args) {
        Runnable[] bugs = {
            () -> { String s = null; s.length(); },
            () -> { int x = 10 / 0; },
            () -> { int[] a = new int[3]; a[3] = 1; },
            () -> { Object o = "text"; Integer i = (Integer) o; },
            () -> Integer.parseInt("12a"),
            () -> List.of(1, 2).iterator().remove(),
        };
        for (Runnable bug : bugs) {
            try {
                bug.run();
            } catch (RuntimeException e) {
                System.out.println(e.getClass().getSimpleName() + ": " + e.getMessage());
            }
        }
    }
}
$ java Unchecked.java
NullPointerException: Cannot invoke "String.length()" because "<local0>" is null
ArithmeticException: / by zero
ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer (java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap')
NumberFormatException: For input string: "12a"
UnsupportedOperationException: null

All six compiled without complaint. Modern JDKs give “helpful” NullPointerException messages naming what was null; <local0> appears instead of s because the source launcher compiles without local-variable debug info. UnsupportedOperationException has no message at all, so getMessage() returns null.

Reading a stack trace

An uncaught exception ends the program and prints the call stack, newest call first.

public class Trace {
    public static void main(String[] args) {
        System.out.println(average(new int[] {}));
    }

    static int average(int[] values) {
        return sum(values) / values.length;
    }

    static int sum(int[] values) {
        int total = 0;
        for (int v : values) total += v;
        return total;
    }
}
$ java Trace.java
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at Trace.average(Trace.java:7)
	at Trace.main(Trace.java:3)

Read top-down: the exception and message, then where it was thrown (average, line 7), then who called that (main, line 3). sum doesn’t appear because it had already returned. Dividing an int by zero throws, but a double division by zero gives Infinity instead.

Error: the JVM itself is in trouble

Errors are unchecked too, but they signal conditions an application can’t fix, like running out of stack or heap.

public class Overflow {
    static int depth = 0;

    static void recurse() {
        depth++;
        recurse();          // no base case
    }

    public static void main(String[] args) {
        try {
            recurse();
        } catch (StackOverflowError e) {
            System.out.println("StackOverflowError after roughly " + (depth / 1000) * 1000 + " calls (varies)");
            Throwable t = e;
            System.out.println("is an Exception? " + (t instanceof Exception));
        }
    }
}
$ java Overflow.java
StackOverflowError after roughly 20000 calls (varies)
is an Exception? false

The depth depends on the JVM’s stack size and the frame size, so your number will differ. StackOverflowError is not an Exception, so a catch (Exception e) would not stop it. (The compiler knows this too: written directly as e instanceof Exception, the test is rejected as impossible, which is why it goes through a Throwable variable.) It is caught here only to print the demonstration; real code fixes the missing base case.

Wrapped exceptions: “Caused by”

Libraries often wrap a low-level exception in a higher-level one. The original is kept as the cause and printed after Caused by:.

public class Wrapped {
    public static void main(String[] args) {
        loadPort("http-port=eighty");
    }

    static int loadPort(String line) {
        try {
            return Integer.parseInt(line.split("=")[1]);
        } catch (NumberFormatException e) {
            throw new IllegalStateException("bad config line: " + line, e);
        }
    }
}
$ java Wrapped.java
Exception in thread "main" java.lang.IllegalStateException: bad config line: http-port=eighty
	at Wrapped.loadPort(Wrapped.java:10)
	at Wrapped.main(Wrapped.java:3)
Caused by: java.lang.NumberFormatException: For input string: "eighty"
	at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
	at java.base/java.lang.Integer.parseInt(Integer.java:662)
	at java.base/java.lang.Integer.parseInt(Integer.java:778)
	at Wrapped.loadPort(Wrapped.java:8)
	at Wrapped.main(Wrapped.java:3)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
	at java.base/java.lang.reflect.Method.invoke(Method.java:580)
	at jdk.compiler/com.sun.tools.javac.launcher.Main.execute(Main.java:484)
	at jdk.compiler/com.sun.tools.javac.launcher.Main.run(Main.java:208)
	at jdk.compiler/com.sun.tools.javac.launcher.Main.main(Main.java:135)

The top exception says what the program was doing; the cause says why it failed. The jdk.compiler/...launcher.Main frames at the bottom are the source launcher calling your main; they appear because this program was started with java Wrapped.java and are absent when you run a compiled class.

Practice

  1. Without running it, predict whether each line compiles, and if it runs, which exception it throws. Then run the program.
Solution
public class Predict {
    public static void main(String[] args) {
        Object[] tries = new Object[3];
        try { tries[0] = "abc".charAt(5); } catch (RuntimeException e) { System.out.println("1: " + e.getClass().getSimpleName()); }
        try { tries[1] = Long.parseLong(" 42"); } catch (RuntimeException e) { System.out.println("2: " + e.getClass().getSimpleName()); }
        try { tries[2] = ((Object) 3.0) instanceof Integer i ? i : (Integer) (Object) 3.0; } catch (RuntimeException e) { System.out.println("3: " + e.getClass().getSimpleName()); }
        System.out.println(10.0 / 0);
    }
}
$ java Predict.java
1: StringIndexOutOfBoundsException
2: NumberFormatException
3: ClassCastException
Infinity

All four lines compile — unchecked exceptions are only discovered at run time. parseLong rejects the leading space, a Double can’t be cast to Integer, and floating-point division by zero is not an error at all.

  1. Fix Trace.java so average of an empty array returns 0 instead of crashing.
Solution
public class TraceFixed {
    public static void main(String[] args) {
        System.out.println(average(new int[] {}));
        System.out.println(average(new int[] {2, 4, 9}));
    }

    static int average(int[] values) {
        if (values.length == 0) return 0;
        int total = 0;
        for (int v : values) total += v;
        return total / values.length;
    }
}
$ java TraceFixed.java
0
5

A guard clause prevents the bug instead of catching ArithmeticException after the fact. (Note the integer average of 2, 4, 9 is 5, not 5.0.)

  1. Make Checked.java compile by declaring the exception instead of catching it. What does the output look like when the file is missing?
Solution
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class Declared {
    public static void main(String[] args) throws IOException {
        System.out.println(Files.readString(Path.of("config.txt")));
    }
}
$ java Declared.java
Exception in thread "main" java.nio.file.NoSuchFileException: config.txt
	at java.base/sun.nio.fs.UnixException.translateToIOException(UnixException.java:92)
	at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:106)
	at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:111)
	at java.base/sun.nio.fs.UnixFileSystemProvider.newByteChannel(UnixFileSystemProvider.java:261)
	at java.base/java.nio.file.Files.newByteChannel(Files.java:380)
	at java.base/java.nio.file.Files.newByteChannel(Files.java:432)
	at java.base/java.nio.file.Files.readAllBytes(Files.java:3281)
	at java.base/java.nio.file.Files.readString(Files.java:3359)
	at java.base/java.nio.file.Files.readString(Files.java:3318)
	at Declared.main(Declared.java:7)

throws passes the responsibility up; from main it reaches the JVM, which prints the trace and exits. The JDK frames (sun.nio.fs...) vary by operating system — on Windows they mention WindowsException — but the last line still points at your code.

  1. Add a Caused by chain two levels deep: wrap the IllegalStateException from Wrapped in a RuntimeException("startup failed", e) and catch it in main, printing each cause’s message.
Solution
public class Chain {
    public static void main(String[] args) {
        try {
            start();
        } catch (RuntimeException e) {
            for (Throwable t = e; t != null; t = t.getCause()) {
                System.out.println(t.getClass().getSimpleName() + ": " + t.getMessage());
            }
        }
    }

    static void start() {
        try {
            Integer.parseInt("eighty");
        } catch (NumberFormatException e) {
            try {
                throw new IllegalStateException("bad config", e);
            } catch (IllegalStateException ise) {
                throw new RuntimeException("startup failed", ise);
            }
        }
    }
}
$ java Chain.java
RuntimeException: startup failed
IllegalStateException: bad config
NumberFormatException: For input string: "eighty"

getCause() walks from the outermost wrapper down to the root cause, which returns null for its own cause and ends the loop.

Frequently Asked Questions

What is the difference between checked and unchecked exceptions?
Checked exceptions (IOException, SQLException, etc.) must be declared with 'throws' or caught — the compiler enforces this. Unchecked exceptions extend RuntimeException (NullPointerException, IllegalArgumentException, etc.) and do not need to be declared. Use checked exceptions for recoverable conditions the caller should handle; use unchecked for programming errors.
When should I catch Exception vs a specific exception type?
Catch the most specific type you can actually handle. Catching generic Exception can hide bugs and makes it hard to know what went wrong. The usual reason to catch Exception broadly is at a top-level boundary, such as a web request handler or main, to log the failure and return a clean error.
Does finally always run?
Almost always. The finally block runs even if an exception is thrown, even if there is a return in the try block. The only exceptions are: System.exit() is called, the JVM crashes, or the thread is killed. In Java 7+ prefer try-with-resources over finally for closing resources.
What is the difference between throw and throws?
throw is a statement inside a method body that raises an exception instance, as in throw new IllegalArgumentException("bad input"). throws is part of a method signature and declares that the method may let a checked exception escape, as in void readFile() throws IOException.
How do I read a Java stack trace?
The first line names the exception class and its message. The 'at' lines below it are the call stack, most recent call first, each with a file name and line number; the first line that points into your own code is usually where to look. A 'Caused by:' section shows the original exception that was wrapped.