AlgoInsight

Free lesson · Java Basic To Advance

Introduction to Exception Handling

Medium6 min readjava

Handling What Goes Wrong

Code fails. Files don't exist. Network connections drop. Users enter bad input. Databases throw errors. Disk space runs out. Memory allocations fail. Every non-trivial program has to handle things going wrong.

Exception handling is Java's mechanism for dealing with these failures. It's a structured way to detect errors, propagate them up the call stack, and respond to them at the right level — separating the "what to do when things work" code from the "what to do when things break" code.

Brandon Carter back for Module 12. Exception handling is one of the most-debated topics in Java. Checked vs unchecked exceptions, when to throw, when to catch, what to log — these decisions affect how robust and maintainable a codebase is. I've seen exception handling done poorly destroy systems in production, and done well make codebases self-healing.

What an Exception Is

An exception is an object representing an error or unusual condition. When something goes wrong, code can throw an exception. Code higher in the call stack can catch it and respond.

public int divide(int a, int b) {
    if (b == 0) {
        throw new ArithmeticException("Cannot divide by zero");
    }
    return a / b;
}

public void calculate() {
    try {
        int result = divide(10, 0);
        System.out.println(result);
    } catch (ArithmeticException e) {
        System.out.println("Error: " + e.getMessage());
    }
}

divide throws an exception when called with zero. calculate catches it and handles the error. The normal flow (printing the result) is separate from the error flow (printing the error).

This separation is the core value of exceptions. Without them, error handling clutters every line of code:

// Pre-exception style — error codes everywhere
public int divide(int a, int b, ErrorCode error) {
    if (b == 0) {
        error.set(DIVISION_BY_ZERO);
        return -1;
    }
    error.set(NONE);
    return a / b;
}

public void calculate() {
    ErrorCode error = new ErrorCode();
    int result = divide(10, 0, error);
    if (error.value() == DIVISION_BY_ZERO) {
        System.out.println("Division error");
        return;
    }
    System.out.println(result);
}

Every call has to check for errors. Error handling logic is interleaved with normal logic. Exceptions clean this up dramatically.

The Three Players

Exception handling involves three operations:

Throw: Signal that something went wrong.

throw new IOException("File not found");

Catch: Respond to a thrown exception at some point up the call stack.

try {
    readFile();
} catch (IOException e) {
    log.error("Read failed", e);
}

Propagate: Let the exception bubble up to a higher caller if you can't handle it here.

public String readData() throws IOException {
    return readFile();   // exception propagates to caller
}

A program uses all three. The classes throwing exceptions are usually low-level (file I/O, network, parsing). The classes catching them are usually higher-level (controllers, handlers, main loops). Propagation moves the exception from low-level to high-level until something can handle it.

What Happens When an Exception is Thrown

Mechanically, when an exception is thrown:

  1. The current method stops executing immediately.
  2. The JVM looks up the call stack for a catch block matching the exception type.
  3. If found, control jumps to that catch block.
  4. If not found anywhere up the stack, the thread terminates (and if it's the main thread, the JVM exits with an error).

Anything after the throw statement in the throwing method doesn't run. Pending operations are skipped. Open resources may leak (unless cleaned up in a finally block or try-with-resources — Topics 12.4 and 12.7).

This stack-unwinding behavior is why exceptions are powerful but also why they need to be used carefully. Throwing an exception isn't "just like returning an error." It's a control-flow disruption.

Why Exceptions Beat Error Codes

Most modern languages use exceptions over return-value error codes for several reasons:

1. Cleaner code. Error handling separates from the main logic, not interleaved with it.

2. Forced visibility. Unhandled exceptions terminate the program (or are visible in logs). Ignored return codes silently fail.

3. Type information. An exception's class indicates what went wrong. Catching an IOException vs SQLException vs IllegalArgumentException provides specific information.

4. Stack trace. Exceptions carry information about where they originated — invaluable for debugging.

5. Multiple error types. A single method can throw different exception types for different errors. Return codes typically encode this awkwardly.

Some languages (Go, Rust) have moved back toward return-based error handling for different reasons (predictability, performance). Both approaches have merits. Java firmly chose exceptions, and the rest of the ecosystem follows.

A Real Production Story

At Amazon S3, our request handler framework has thousands of operation types. Each operation can fail in dozens of ways — network errors, authentication failures, throttling, validation errors, storage errors, etc.

Early in the framework's design, we tried using error codes returned from each handler. The result was unmanageable: every handler had to check return values from every downstream call, every error had to be propagated up manually, and adding a new error type meant updating every layer of the system.

We refactored to exceptions. Now each handler throws specific exception types — ThrottlingException, ValidationException, AuthFailureException, StorageException. Higher-level code catches them by type and responds appropriately (retry on throttling, reject on validation, etc.). Adding a new error type means adding the class — no changes to existing code that doesn't handle it.

The refactor took six months. The maintenance cost dropped by an estimated 40% afterwards. New developers could write handlers correctly without reading thousands of lines of error-handling code. Error reporting became consistent across the system.

This is what good exception handling earns at scale. The framework is more robust, easier to extend, and easier for new developers to learn.

What This Module Covers

Module 12 covers exception handling in depth:

After this module, you'll be able to design exception hierarchies, choose between checked and unchecked exceptions, handle errors at the right level, and avoid common pitfalls.

A Quick Mental Model

Think of exceptions as "control flow for the unexpected":

Both are forms of control flow. They have different rules and serve different purposes. Code that uses both well stays readable; code that mixes them up becomes confusing.

graph TD
    A["Code runs normally"] --> B{"Something<br/>goes wrong?"}
    B -->|No| C["Normal flow continues"]
    B -->|Yes| D["throw new Exception()"]

    D --> E["Method exits immediately"]
    E --> F["JVM searches stack for catch"]
    F --> G{"Catch found?"}

    G -->|Yes| H["Jump to catch block"]
    G -->|No| I["Thread terminates"]

    style D fill:#dc2626,color:#fff
    style H fill:#10b981,color:#fff
    style I fill:#7c3aed,color:#fff
This diagram shows the high-level flow of exception handling. Code runs normally until something goes wrong. When code throws an exception, the current method exits immediately. The JVM searches up the call stack looking for a try-catch block that matches the exception type. If found, control jumps to that catch block, which handles the error. If no matching catch is found anywhere up the stack, the thread terminates. For the main thread, this means the JVM exits. This is why uncaught exceptions are visible — they don't silently fail, they crash the program.

Common Pitfalls Before We Dive In

A preview of what's ahead:

Warning: Catching and ignoring exceptions. The classic anti-pattern: catch (Exception e) { }. We'll cover why this destroys production systems in Topic 12.10.

Catching too broadly. catch (Exception e) catches everything — usually not what you want. Catch specific types.

Throwing exceptions for normal control flow. Exceptions are expensive. Don't use them for things that aren't actually exceptional.

Letting resources leak. Without try-with-resources or try-finally, exceptions can leave files, connections, locks unclosed.

Missing the chain. When wrapping exceptions, preserve the original cause. Otherwise debugging becomes impossible.

Why This Matters in Interviews

Interview: Exception handling questions appear in nearly every Java interview at every level. "What's the difference between checked and unchecked exceptions?" "When would you throw vs catch?" "What's wrong with catch (Exception e) { }?"

Senior questions probe design: "How would you design an exception hierarchy for a payment system?" "When should you use custom exceptions?" Topic 12.11 covers the Interview Lens.

Try It

Exercise: Write each:

  1. A method that throws an exception. Another method that catches it. Verify the flow.
  2. A method that doesn't catch an exception. Run it — observe the thread terminating.
  3. A method that throws a custom exception with a message. Catch it and read the message.
  4. A chain: low-level method throws, mid-level method propagates, high-level method catches. Trace the flow.
  5. Identify a piece of code you've written that uses error return codes. Refactor it to use exceptions. Compare readability.

Each exercise builds basic exception intuition before we cover the specifics.

Back to Java Basic To Advance