Stage 3: Core APIs, lesson 1 of 12

Exception handling

Intermediate5 min read@since 7Code runs on your Java 25
Explain it forThe essentials plus production detail and pitfalls.

All errors extend Throwable:

  • Error: serious JVM problems such as OutOfMemoryError. Don't catch these.
  • Checked exceptions (IOException, SQLException): the compiler forces you to catch or declare them.
  • Unchecked exceptions (RuntimeException and subclasses such as NullPointerException and IllegalArgumentException): usually programming bugs.

try-with-resources (Java 7) closes anything that implements AutoCloseable, even when an exception is thrown. Multi-catch (catch (A | B e)) handles several exception types in one block.

Since Java 14, helpful NullPointerExceptions tell you exactly which variable or call was null.

The exception hierarchy

Everything thrown extends Throwable:

  • Error: serious JVM problems such as OutOfMemoryError and StackOverflowError. Don't catch these.
  • Exception: problems a program can handle.
  • RuntimeException (a subclass of Exception): programming errors such as NullPointerException, IllegalArgumentException and IndexOutOfBoundsException.

Checked vs unchecked exceptions

Checked exceptions (IOException, SQLException) must be caught or declared with throws; the compiler enforces it. Unchecked exceptions (RuntimeException and its subclasses) don't have to be. Use checked exceptions for recoverable situations the caller must think about; use unchecked for bugs and invalid arguments. Modern frameworks such as Spring mostly use unchecked exceptions.

try, catch and finally

Code that may fail goes in try. Each catch handles one kind of exception; put the most specific first. finally runs whether or not an exception happened (unless the JVM exits), which makes it the classic place for clean-up.

Java
try {
    int n = Integer.parseInt(input);
    System.out.println(100 / n);
} catch (NumberFormatException e) {
    System.out.println("Please enter a number");
} catch (ArithmeticException e) {
    System.out.println("Can't divide by zero");
} finally {
    System.out.println("Done");
}

Multi-catch

Since Java 7 one catch can handle several unrelated exception types with the same code. The types can't be subclasses of each other.

Java
try {
    loadConfig();
} catch (IOException | IllegalStateException e) {
    log.error("Couldn't load config", e);
}

try-with-resources

Anything that implements AutoCloseable (files, streams, connections) can be declared in the try header; Java closes it automatically, in reverse order, even if an exception is thrown. If closing also fails, that exception is attached as suppressed instead of hiding the original.

Java
try (var in = Files.newBufferedReader(path);
     var out = Files.newBufferedWriter(copy)) {
    in.transferTo(out);
} // both closed here, out first, then in

// Java 9+: an existing effectively final resource
BufferedReader reader = Files.newBufferedReader(path);
try (reader) { System.out.println(reader.readLine()); }

throw and throws

throw raises an exception object right now. throws in a method signature declares checked exceptions the method may pass on to its caller.

Java
public Course find(String slug) throws CourseNotFoundException {
    return repo.findBySlug(slug)
               .orElseThrow(() -> new CourseNotFoundException(slug));
}

void setAge(int age) {
    if (age < 0) throw new IllegalArgumentException("age can't be negative: " + age);
}

Custom exceptions

Create your own exception when callers need to handle a specific situation, or when you want a clear name in logs. Extend RuntimeException (unchecked) or Exception (checked), pass a helpful message, and add fields for context.

Java
public class InsufficientBalanceException extends RuntimeException {
    private final long shortByPaise;

    public InsufficientBalanceException(long shortByPaise) {
        super("Balance is short by " + shortByPaise / 100.0 + " rupees");
        this.shortByPaise = shortByPaise;
    }

    public long shortByPaise() { return shortByPaise; }
}

Exception chaining (keeping the cause)

When you translate a low-level exception into a higher-level one, pass the original as the cause. The stack trace then shows both, and nothing is lost for debugging.

Java
try {
    return jdbc.query(sql);
} catch (SQLException e) {
    throw new DataAccessException("Couldn't load orders for user " + userId, e);   // e is the cause
}

Best practices

  • Never swallow exceptions with an empty catch; at least log them.
  • Catch the most specific type you can handle; avoid catch (Exception e) except at the top level.
  • Don't use exceptions for normal control flow; they're slow and hide intent.
  • Validate early: Objects.requireNonNull, argument checks at the start of methods.
  • Log an exception once, where it's handled, not at every layer.
  • Include context in messages (ids, values), but never secrets.

finally pitfalls

A return inside finally silently replaces any exception or earlier return value, and an exception thrown in finally hides the original one. Keep finally for clean-up only, or better, use try-with-resources.

Java
static int broken() {
    try {
        throw new IllegalStateException("real problem");
    } finally {
        return 0;     // the exception disappears: never do this
    }
}

Checked exceptions in lambdas and streams

Standard functional interfaces don't allow checked exceptions, so a lambda that calls, say, Files.readString won't compile as is. Handle the exception inside the lambda, or wrap it in an unchecked exception such as UncheckedIOException.

Java
List<String> contents = paths.stream()
    .map(p -> {
        try {
            return Files.readString(p);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    })
    .toList();

Handling exceptions globally in Spring Boot

In a REST API, don't wrap every controller method in try-catch. A @RestControllerAdvice class turns exceptions into consistent JSON error responses (ProblemDetail, RFC 9457) in one place. The REST API lesson shows the full setup.

Java
@RestControllerAdvice
class ApiErrors {
    @ExceptionHandler(CourseNotFoundException.class)
    ProblemDetail notFound(CourseNotFoundException e) {
        return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, e.getMessage());
    }
}

Old way vs new way

Before Java 7
BufferedReader reader = null;
try {
    reader = new BufferedReader(new FileReader(file));
    return reader.readLine();
} finally {
    if (reader != null) {
        try { reader.close(); } catch (IOException ignored) { }
    }
}
Java 7+ try-with-resources
public String firstLine(Path file) {
    try (BufferedReader reader = Files.newBufferedReader(file)) {   // closed automatically
        return reader.readLine();
    } catch (NoSuchFileException | AccessDeniedException e) {
        throw new CourseFileException("Cannot open " + file, e);    // keep the cause
    } catch (IOException e) {
        throw new UncheckedIOException(e);
    }
}

class CourseFileException extends RuntimeException {
    CourseFileException(String msg, Throwable cause) { super(msg, cause); }
}

Common mistake

Writing catch (Exception e) { } and silently swallowing the error. At minimum log it; usually rethrow it wrapped, with the original as the cause.

Under the hood

If both the try block and close() throw, the close exception is attached as a suppressed exception (getSuppressed()) instead of hiding the original. Creating exceptions is relatively expensive because of the stack trace, so use them for exceptional cases, not normal control flow. In Spring, @Transactional rolls back on unchecked exceptions by default but not on checked ones unless you set rollbackFor.

Check yourself

Which interface must a resource implement to be used in try-with-resources?

How this connects

Part of Java from zero, Job-ready backend developer, Crack the Java interview.

Was this lesson helpful?

Finished reading? Mark it complete to track your progress.