← Back to the section

Sooner or later, every program hits the point where something goes wrong: a file is not found, the network drops, a method receives null. An exception is Java's way of saying "this code can't continue" and handing control to a place where the error can be handled.

Why exceptions at all

You could return an error code from a method instead — say -1 or null. But then you'd have to write a "did it break?" check after every call, and it's easy to forget one. Exceptions solve this differently: the problematic code is interrupted, and the error "bubbles up" the call stack until someone catches it.

Short formula: an exception separates the normal path from error handling — they aren't mixed in one flow of code.

call stack frames Integer.parseInt("abc") throw NumberFormatException parse(s) no matching catch here main catch (NumberFormatException e) exception

The exception from parseInt travels up the stack frames. A frame with no matching catch is discarded along with its local variables; execution continues in the first frame where the type matched.

live example

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

    public static void main(String[] args) {
        System.out.println("number: " + parse("42"));
        try {
            parse("abc");
        } catch (NumberFormatException e) {
            System.out.println("not a number: " + e.getMessage());
        }
        System.out.println("the program keeps running");
    }
}
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

On "abc" the method returns no garbage: it is interrupted, and the caller decides what happens next.

The hierarchy: Throwable, Error, Exception

At the root of everything sits the Throwable class — only its subclasses can be thrown (throw) and caught (catch). It has two main branches:

  • Error — serious failures of the JVM itself: OutOfMemoryError, StackOverflowError. These are not caught in place: fixing that in a catch in the middle of business logic is pointless, the program is already in a bad state. The only sensible place is the very top: there you catch Throwable to log the cause and stop gracefully instead of dying silently. Running out of memory is usually handled without any code at all — the -XX:+ExitOnOutOfMemoryError flag terminates the process so it can be restarted.
  • Exception — application-level errors that you can and should work with.

Inside Exception there is a special sub-branch — RuntimeException. It is precisely this line that marks the border between checked and unchecked exceptions.

diagram

Checked and unchecked

This split is one of Java's most debated features.

Checked exceptions — everything that inherits Exception but not RuntimeException (for example IOException, SQLException). The compiler forces you to handle them: either wrap the call in a try/catch, or declare it in the method signature via throws. Fail to do so, and the code won't compile. Both ways fit in one program:

live example

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

public class ConfigDemo {
    static String read(Path path) throws IOException {
        return Files.readString(path);
    }

    static String readOrDefault(Path path) {
        try {
            return read(path);
        } catch (IOException e) {
            return "port=8080";
        }
    }

    public static void main(String[] args) {
        System.out.println(readOrDefault(Path.of("config.properties")));
    }
}
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

The file is missing, Files.readString throws IOException — but the program survives: readOrDefault substitutes a default, read passes responsibility upward via throws.

Unchecked exceptions — subclasses of RuntimeException (NullPointerException, IllegalArgumentException, IllegalStateException, NumberFormatException). The compiler doesn't police them: catching is optional. These are usually bugs in the code — dereferencing null, an invalid argument, a violation of a method's contract.

Short formula: checked — "an expected external problem the caller should do something about"; unchecked — "the programmer made a mistake, fix the code rather than catch the exception".

The debate around checked

The idea behind checked exceptions is to force developers not to ignore errors. The criticism is fair too: in long call chains throws IOException drags through dozens of methods, and an empty catch written to shut the compiler up is worse than nothing. Many libraries and frameworks — a large part of the Spring ecosystem included — wrap checked exceptions into runtime wrappers. There is no ready-made "correct" answer; both sides are worth understanding.

try / catch / finally

Risky code goes into try, error handling into catch, cleanup into finally. The three input values take three different paths.

live example

public class CatchOrderDemo {
    static void divide(String value) {
        try {
            System.out.println("quotient: " + 100 / Integer.parseInt(value));
        } catch (ArithmeticException e) {
            System.out.println("cannot divide by zero");
        } catch (RuntimeException e) {
            System.out.println("something else: " + e.getClass().getSimpleName());
        } finally {
            System.out.println("finally — in any case");
        }
    }

    public static void main(String[] args) {
        divide("4");
        divide("0");
        divide("not a number");
    }
}
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

What matters here:

  • Multiple catch blocks are checked top to bottom — the first one matching by type fires. That's why the specific ArithmeticException goes above the general RuntimeException: swap them and the code won't compile, the second branch becomes unreachable.
  • Multi-catch merges branches with identical handling: catch (IOException | SQLException e).
  • finally runs in any case — even after a return inside the try. Releasing resources (closing a file, a connection) historically went here.
  • Catching "everything" via catch (Exception e) is risky — easy to intercept what you never meant to handle.

try-with-resources and AutoCloseable

Manual closing in finally is noisy and easy to get wrong (what if close() itself throws an exception?). Since Java 7 there is try-with-resources for this: resources are declared in parentheses after try and closed automatically, in reverse order, even if an exception occurs.

This works for any class implementing the AutoCloseable interface (it has a single method — close()). Most standard "closeable" types (streams, files, connections) implement it.

live example

public class ResourceDemo {
    record Connection(String name) implements AutoCloseable {
        @Override public void close() {
            System.out.println("closed " + name);
        }
    }

    public static void main(String[] args) {
        try (var file = new Connection("file");
             var socket = new Connection("socket")) {
            System.out.println("working with " + file.name() + ", " + socket.name());
            throw new IllegalStateException("the network dropped");
        } catch (IllegalStateException e) {
            System.out.println("caught: " + e.getMessage());
        }
    }
}
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

The output shows the order: the socket closed before the file, both before the catch. If close() throws too, it doesn't overwrite the original exception — it arrives attached as a suppressed one, available via getSuppressed().

Custom exceptions

When standard types don't convey the meaning of an error in your domain, you create a custom exception. It usually inherits from RuntimeException (if you don't want to impose mandatory handling) or from Exception (if you do).

live example

public class OrderDemo {
    public static void main(String[] args) {
        try {
            loadOrder(42);
        } catch (OrderLoadException e) {
            System.out.println(e.getMessage());
            System.out.println("cause: " + e.getCause());
        }
    }

    static void loadOrder(long id) {
        try {
            long total = Long.parseLong("broken data");
            System.out.println("order total: " + total);
        } catch (NumberFormatException e) {
            throw new OrderLoadException(id, e);
        }
    }
}

class OrderLoadException extends RuntimeException {
    OrderLoadException(long orderId, Throwable cause) {
        super("Failed to load order " + orderId, cause);
    }
}
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

Useful habits:

  • A clear message — what happened and with what data: the output shows the order number, not a faceless "error".
  • Preserve the cause. The original exception goes into the constructor as the second argument — the log then shows the full chain (Caused by: ...), not a broken trail.

Antipatterns

The most common ways to shoot yourself in the foot:

// 1. Empty catch — the exception vanishes without a trace
try {
    risky();
} catch (Exception e) {
    // silence — now no one will ever know what broke
}

// 2. Swallowing with loss of the cause
try {
    risky();
} catch (IOException e) {
    throw new RuntimeException("error"); // lost the original e!
}

// 3. Control flow through exceptions — slow and unreadable
try {
    return list.get(index);
} catch (IndexOutOfBoundsException e) {
    return null; // better to just check index beforehand
}

Do it right: never leave a catch empty (at minimum, log it), always preserve the cause when wrapping, and don't use exceptions as an ordinary if — they're for exceptional situations, not for regular logic.

In short

  • Exceptions separate error handling from the main code: the problematic section is interrupted, the error bubbles up to the first matching catch.
  • At the root is Throwable. Error isn't caught in place (JVM failures), Exception is handled.
  • Checked (subclasses of Exception except RuntimeException) — the compiler forces you to handle them; unchecked (RuntimeException) — it doesn't: those are usually bugs in the code.
  • catch blocks are checked top to bottom, from the specific type to the general one; finally always runs — even with a return inside the try.
  • For resources use try-with-resources (AutoCloseable): it closes them itself, in reverse order, before the catch runs.
  • Custom exceptions give meaningful errors; the main antipatterns — an empty catch, a lost cause, control flow through exceptions.