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.
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 acatchin 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 catchThrowableto log the cause and stop gracefully instead of dying silently. Running out of memory is usually handled without any code at all — the-XX:+ExitOnOutOfMemoryErrorflag 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.
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
catchblocks are checked top to bottom — the first one matching by type fires. That's why the specificArithmeticExceptiongoes above the generalRuntimeException: swap them and the code won't compile, the second branch becomes unreachable. - Multi-catch merges branches with identical handling:
catch (IOException | SQLException e). finallyruns in any case — even after areturninside thetry. 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.Errorisn't caught in place (JVM failures),Exceptionis handled. - Checked (subclasses of
ExceptionexceptRuntimeException) — the compiler forces you to handle them; unchecked (RuntimeException) — it doesn't: those are usually bugs in the code. catchblocks are checked top to bottom, from the specific type to the general one;finallyalways runs — even with areturninside thetry.- For resources use try-with-resources (
AutoCloseable): it closes them itself, in reverse order, before thecatchruns. - Custom exceptions give meaningful errors; the main antipatterns — an empty
catch, a lost cause, control flow through exceptions.
What to read next
- Syntax and data types — the basics everything else stands on.
- Collections — where
IndexOutOfBoundsExceptionandNullPointerExceptionshow up often. - Records, Optional and modern Java — how
Optionalremoves part of the reasons forNullPointerException. - Exception hierarchy in Spring Boot — how the same rules look in a working service.