Java Exception Handling

INTERMEDIATE ~8 min read Tutorial

Exceptions are Java's mechanism for handling unexpected conditions: a file that does not exist, a network call that fails, a user input that does not parse. An exception is an object that carries information about what went wrong; the JVM unwinds the call stack until it finds a matching catch, or until the program terminates.

This tutorial covers the try/catch/finally syntax, the difference between checked and unchecked exceptions, when to throw and when to catch, how to write your own exception classes, and the modern try-with-resources form that closes resources automatically.

1. The Exception Hierarchy

All exceptions descend from java.lang.Throwable. There are two main branches:

  • Error — serious problems a program is unlikely to recover from (OutOfMemoryError, StackOverflowError). Catch them rarely, if ever.
  • Exception — application-level problems. The subclass RuntimeException is unchecked (compiler does not enforce handling); all other Exception subclasses are checked (compiler requires throws or try/catch).
ExceptionTypeExample cause
NullPointerExceptionuncheckedDereferencing a null reference
IllegalArgumentExceptionuncheckedBad method argument
IndexOutOfBoundsExceptionuncheckedArray/list index out of range
ArithmeticExceptionuncheckedInteger division by zero
ClassCastExceptionuncheckedInvalid reference cast
IOExceptioncheckedFile or network I/O failure
SQLExceptioncheckedDatabase error
InterruptedExceptioncheckedThread was interrupted

2. try / catch / finally

Wrap risky code in try; handle the failure in catch; finally runs whether or not an exception was thrown:

java
try {
    riskyOperation();
} catch (IOException e) {
    System.err.println("I/O failed: " + e.getMessage());
} catch (SQLException e) {
    System.err.println("DB failed: " + e.getMessage());
} finally {
    class=class="tok-str">"tok-cmt">// always runs - even if try returned or threw
    cleanup();
}

class=class="tok-str">"tok-cmt">// try-finally without catch is legal but rarely what you want
try {
    doWork();
} finally {
    closeResources();
}

finally is for cleanup that must always run — closing files, releasing locks. Java 7+ added try-with-resources (below) which is now the preferred way to handle closeable resources.

3. Multi-Catch and Exception Chaining

One catch can handle several exception types with |:

java
try {
    parseAndSave(input);
} catch (NumberFormatException | IllegalStateException e) {
    class=class="tok-str">"tok-cmt">// handle both the same way
    log.warn("bad input: {}", input, e);
    throw new IllegalArgumentException("bad input", e);
}

If you catch a low-level exception and want to throw a higher-level one, use the original as the cause so you do not lose the stack trace:

java
class=class="tok-str">"tok-cmt">// chaining - rethrow with the original as the cause
try {
    return Files.readString(Path.of(path));
} catch (IOException original) {
    throw new RuntimeException("Failed to read " + path, original);
    class=class="tok-str">"tok-cmt">//                                                                       ^^^^^^^
    class=class="tok-str">"tok-cmt">//                       original becomes the cause of the new exception
}

4. Throwing Exceptions

Use throw to raise an exception, throws to declare checked exceptions on a method:

java
public void withdraw(double amount) {
    if (amount <= class="tok-num">0)
        throw new IllegalArgumentException("amount must be positive");
    if (amount > balance)
        throw new IllegalStateException("insufficient funds");
    balance -= amount;
}

class=class="tok-str">"tok-cmt">// declaring a checked exception
public String readFile(String path) throws IOException {
    return Files.readString(Path.of(path));
}

class=class="tok-str">"tok-cmt">// caller must handle or declare
try {
    String text = readFile("data.txt");
} catch (IOException e) {
    e.printStackTrace();
}

Throw early: validate arguments at the top of a method. Throw specific types so callers can catch the right thing.

5. try-with-resources

Any object that implements AutoCloseable can be declared as a resource — the JVM calls close() automatically, even if an exception is thrown:

java
class=class="tok-str">"tok-cmt">// try-with-resources - resource closed automatically
try (var reader = Files.newBufferedReader(Path.of("data.txt"));
     var writer = Files.newBufferedWriter(Path.of("out.txt"))) {
    String line;
    while ((line = reader.readLine()) != null) {
        writer.write(line);
        writer.newLine();
    }
}   class=class="tok-str">"tok-cmt">// reader.close() and writer.close() called here, even on exception

class=class="tok-str">"tok-cmt">// multiple resources - closed in reverse order of declaration

This eliminates an entire category of resource leaks. Always prefer it over manual close() in a finally block.

6. Custom Exceptions

Subclass RuntimeException for an unchecked exception, or Exception for a checked one:

java
class=class="tok-str">"tok-cmt">// unchecked - programming error
public class InvalidAgeException extends RuntimeException {
    public InvalidAgeException(int age) {
        super("Invalid age: " + age + " (must be class="tok-num">0-class="tok-num">150)");
    }
}

class=class="tok-str">"tok-cmt">// checked - recoverable condition
public class ServiceUnavailableException extends Exception {
    private final int statusCode;
    public ServiceUnavailableException(int statusCode, String message) {
        super(message);
        this.statusCode = statusCode;
    }
    public int statusCode() { return statusCode; }
}

class=class="tok-str">"tok-cmt">// usage
public void setAge(int age) {
    if (age < class="tok-num">0 || age > class="tok-num">150)
        throw new InvalidAgeException(age);
    this.age = age;
}

Prefer unchecked exceptions for programming errors (bad arguments, illegal state) and checked exceptions for conditions a caller can reasonably recover from (network timeout, file not found). The modern trend in the Java community is toward fewer checked exceptions — many libraries now wrap checked exceptions in unchecked ones to avoid forcing callers to handle them.

7. Best Practices

Never swallow exceptions

catch (Exception e) { /* nothing */ } is one of the worst things you can do. If you must ignore, at least log; if you cannot recover, rethrow wrapped in a runtime exception so the failure surfaces.

Do not catch Throwable

Throwable includes Error — serious JVM-level problems you cannot recover from. Catching them masks real failures.

Order catch from most specific to least

The first matching catch wins. catch (NullPointerException e) before catch (RuntimeException e); otherwise the specific catch is unreachable code and the compiler will complain.

Exercises

  1. Write a method that reads a file with Files.readString and prints its length. Catch IOException and print a friendly message.
  2. Rewrite the file-reading to use try-with-resources with a BufferedReader.
  3. Define a custom InvalidAgeException and throw it from a constructor when the age is negative.
  4. Write a method that catches NumberFormatException and rethrows it wrapped in an IllegalArgumentException with the original as cause.