Skip to content
Chapter 20 · Core Java

Exception Handling in Java ​

Every program you write is a set of instructions running one after another in a predictable sequence. Your code opens a file, reads some data, does a calculation, stores a result. That sequence is your program's normal flow. But real programs live in a messy world where things go wrong. A file disappears. Someone passes a negative number where only positive numbers make sense. A network connection drops mid transfer. When any of these unexpected events hit your running program and break that normal flow, Java calls it an exception.

The word "exception" is deliberately chosen. It is an exceptional event, something outside the happy path, something that your program did not expect to encounter at that point. When one of these events occurs during execution, the Java runtime does something very specific. It creates an exception object. That object carries three critical pieces of information: the type of exception that occurred, a human readable message explaining what went wrong, and something called a stack trace. You will see all three of these every time your program crashes with an unhandled exception in the console output.

What Is a Stack Trace ​

A stack trace is a record of the execution path from the point where the exception occurred all the way back to where your program started. Imagine your main method calls methodOne, methodOne calls methodTwo, and methodTwo calls methodThree. If an exception fires inside methodThree, the stack trace shows you exactly that chain: methodThree at line 18, called by methodTwo at line 14, called by methodOne at line 10, called by main at line 6.

The runtime uses this stack trace to search for someone who can handle the exception. It starts at the method where the exception occurred and asks: can you handle this? If not, it moves up to the caller and asks again. It keeps climbing the call chain until it finds a handler. If it reaches the top of the chain without finding anyone willing to handle the exception, the program terminates abruptly and prints that entire stack trace to the console. That is the crash output you have probably already seen.

java
public class Main {
    public static void main(String[] args) {
        methodOne();
    }

    static void methodOne() {
        methodTwo();
    }

    static void methodTwo() {
        methodThree();
    }

    static void methodThree() {
        // This causes ArithmeticException: / by zero
        int result = 5 / 0;
    }
}

When you run this, the output shows the type (ArithmeticException), the message (/ by zero), and the full chain of calls from methodThree back to main. Nobody in the chain handled it, so the program dies.

The Exception Hierarchy ​

To understand exception handling deeply you need to understand the class hierarchy that Java uses. At the top of every Java class hierarchy sits Object. One of its subclasses is Throwable. Everything that can be thrown in Java must be a Throwable or a descendant of it.

Throwable has two direct children: Error and Exception. This is one of the most important distinctions in Java and it comes up in interviews constantly.

Error represents problems that are outside your control and outside your program's control entirely. These are JVM level problems. The two classic examples are OutOfMemoryError and StackOverflowError. When the JVM runs out of heap space to create new objects, it throws OutOfMemoryError. When your stack memory fills up, typically because of infinite recursion where a method keeps calling itself with no exit condition, you get StackOverflowError. These are not things your code caused in a way you can fix at runtime. You should not try to catch or handle errors. They represent a situation so dire that the JVM itself is telling you something is fundamentally broken.

java
// This triggers OutOfMemoryError if the array is huge enough
String[] massiveArray = new String[Integer.MAX_VALUE];

// This triggers StackOverflowError
public static void infiniteRecursion() {
    infiniteRecursion(); // No base case, calls itself forever
}

Exception is the branch you care about. These represent problems that are related to your code, and you have control over them. You can catch them, handle them gracefully, and let your program continue running. This is what exception handling is all about.

Checked vs Unchecked: The Two Flavors of Exception ​

Under Exception there are two categories: checked exceptions and unchecked exceptions. You will also hear them called compile time exceptions and runtime exceptions respectively. The names tell you exactly when the problem surfaces.

Unchecked exceptions (also called runtime exceptions) are problems that only show up when you actually run the program. The compiler does not force you to handle them. Your code compiles perfectly fine whether you handle them or not. But when execution hits the problematic line at runtime, the exception fires. All runtime exceptions extend RuntimeException, which itself extends Exception.

Checked exceptions (also called compile time exceptions) are problems that the compiler actively checks for. If your code can throw a checked exception and you have not handled it properly, the Java compiler refuses to compile your code. You will see a compile error. The compiler is forcing you to deal with it before your program can even run.

This is a crucial design choice in Java. With unchecked exceptions, the compiler trusts you to handle them if you want but does not demand it. With checked exceptions, the compiler says: this situation is serious enough that I will not let your code run unless you have a plan for it.

Is Error Checked or Unchecked? ​

This is a classic interview question. Errors are unchecked. They are categorized with runtime behavior because they happen while the program is running, not at compile time. The compiler never forces you to handle an OutOfMemoryError. Errors are in the unchecked category.

The Runtime Exceptions You Will See Every Day ​

These are the exceptions that extend RuntimeException. The compiler will never warn you about them, but you will encounter each of them in real development.

ClassCastException happens when you try to cast an object to a type that it is not. If you store an Integer in an Object reference and then try to cast it to a String, Java will throw ClassCastException at runtime because an Integer is not a String.

java
Object value = 42; // Integer stored in Object reference
// This compiles fine, but crashes at runtime
String text = (String) value; // ClassCastException: Integer cannot be cast to String

ArithmeticException happens when you perform an illegal arithmetic operation. The most common example is dividing an integer by zero.

java
int result = 5 / 0; // ArithmeticException: / by zero

ArrayIndexOutOfBoundsException happens when you try to access an index that does not exist in an array. An array of size 2 has valid indices 0 and 1. Accessing index 2 throws this exception.

java
int[] values = new int[2]; // Valid indices: 0 and 1
System.out.println(values[3]); // ArrayIndexOutOfBoundsException

StringIndexOutOfBoundsException is the String equivalent. The string "hello" has characters at indices 0 through 4. Calling charAt(5) throws this exception.

java
String word = "hello"; // Characters at indices 0,1,2,3,4
char ch = word.charAt(5); // StringIndexOutOfBoundsException

NullPointerException is perhaps the most famous Java exception. It happens when you try to call a method or access a field on a reference that is null. You cannot call .charAt() on nothing.

java
String value = null;
char ch = value.charAt(0); // NullPointerException: value is null

NumberFormatException happens when you try to parse a string as a number but the string is not a valid number. Parsing "53" works fine. Parsing "ABC" does not.

java
int number = Integer.parseInt("53");  // Works, gives 53
int broken = Integer.parseInt("ABC"); // NumberFormatException

Every single one of these compiles without complaint. The compiler does not warn you. You only discover the problem at runtime, which is why they are called runtime exceptions.

The Checked Exceptions You Must Handle ​

These exceptions live directly under Exception, not under RuntimeException. The compiler treats them specially and will refuse to compile your code if they are not handled. Common examples include ClassNotFoundException, InterruptedException, IOException, FileNotFoundException, EOFException, and SQLException. You will encounter most of these when working with files, databases, and network connections.

The reason these exist is that the situations they represent are predictable and recoverable. A file not being found is something you can plan for. A database connection failing is something you can handle. Java's designers made these checked because ignoring them silently would lead to serious bugs in production software.

The Five Keywords of Exception Handling ​

Java gives you five keywords to work with exceptions: try, catch, finally, throw, and throws. Every piece of exception handling code you write uses some combination of these five.

try and catch: The Core Pattern ​

The try block is where you put code that might throw an exception. The catch block is where you put the code that runs when a specific exception type is thrown from inside the try block.

java
public static void methodOne() throws ClassNotFoundException {
    throw new ClassNotFoundException("Resource not found");
}

public static void main(String[] args) {
    try {
        // This code might throw ClassNotFoundException
        methodOne();
    } catch (ClassNotFoundException e) {
        // Handle it here: log it, show a user message, take corrective action
        System.out.println("Caught: " + e.getMessage());
    }
}

When methodOne throws the exception, execution immediately jumps out of the try block and into the matching catch block. The code after the throw inside the try block is skipped. Inside the catch block, you have full access to the exception object and can do whatever makes sense: log it, display a friendly message, retry the operation, or anything else.

throws: Delegating Responsibility ​

throws is different from throw. It is a declaration on a method signature that tells the world: this method might produce this kind of exception, and I am not going to handle it here. Whoever calls me must handle it.

java
// methodOne declares that it might throw ClassNotFoundException
// It does not handle it; it passes the responsibility to whoever calls it
public static void methodOne() throws ClassNotFoundException {
    throw new ClassNotFoundException("Class not found");
}

throws only appears after a method's parameter list, separated by a comma if there are multiple exception types. It is a contract with the caller. When you use throws, the caller now has the same obligation: they must either handle it with try and catch, or they must also declare throws and pass the responsibility further up.

The key difference: throws is a declaration that says "this might happen." throw is the actual action of making it happen.

throw: Creating and Rethrowing Exceptions ​

You use the throw keyword in two situations. The first is to throw a new exception when something goes wrong in your code:

java
public static void greetUser(String name) {
    if (name.equals("dummy")) {
        throw new IllegalArgumentException("Invalid name provided");
    }
    System.out.println("Hello, " + name);
}

The second use is rethrowing an exception that you already caught:

java
public static void main(String[] args) {
    try {
        methodOne();
    } catch (ClassNotFoundException e) {
        // Do some logging or specific action first
        System.out.println("Logging: exception caught at main level");
        // Now rethrow it, forcing the JVM to terminate since nobody above handles it
        throw e;
    }
}

You might wonder: if I am going to rethrow it anyway, why catch it at all? The answer is that rethrowing is useful when you need to do something specific at this layer, like adding a log message with context that only this method has, before letting the exception continue up the stack. You catch it, do your layer specific work, and then rethrow so the caller can also handle it or let it terminate.

Multiple catch Blocks: Handling Different Exception Types ​

A single try block can have multiple catch blocks, each handling a different exception type:

java
public static void methodOne() throws ClassNotFoundException, InterruptedException {
    // This method might throw either exception
}

public static void main(String[] args) {
    try {
        methodOne();
    } catch (ClassNotFoundException e) {
        System.out.println("Class not found: " + e.getMessage());
    } catch (InterruptedException e) {
        System.out.println("Thread interrupted: " + e.getMessage());
    }
}

There is a critical rule about the order of catch blocks: you must always put the more specific exception type before the more general one. Exception is the parent of almost all exceptions, which means a catch (Exception e) block can catch anything. If you put it first, every exception will match it and your specific catch blocks below will never execute. The compiler actually catches this mistake and marks the unreachable catch blocks with an error.

java
// WRONG: catch (Exception e) first swallows everything
try {
    methodOne();
} catch (Exception e) {           // Too general, catches everything
    System.out.println("Generic");
} catch (ClassNotFoundException e) { // This line will never be reached
    System.out.println("Specific");   // Compiler error: already caught above
}

// RIGHT: specific before general
try {
    methodOne();
} catch (ClassNotFoundException e) { // More specific, checked first
    System.out.println("Specific");
} catch (Exception e) {              // General fallback
    System.out.println("Generic fallback");
}

Multi catch: One catch Block for Multiple Types ​

If two exception types require identical handling code, you do not need two separate catch blocks. Java allows you to catch multiple exception types in a single catch block using the pipe symbol:

java
try {
    methodOne(); // Can throw ClassNotFoundException or InterruptedException
} catch (ClassNotFoundException | InterruptedException e) {
    // Same handling for both
    System.out.println("Either class not found or interrupted: " + e.getMessage());
} catch (Exception e) {
    // Catch any other exception
    System.out.println("Something else went wrong");
}

This keeps your code clean when the handling logic is the same regardless of which specific exception fired.

finally: The Block That Always Runs ​

The finally block runs after the try block and after any catch block, no matter what happens. It runs whether the try block succeeded, whether an exception was thrown and caught, and even if you use return inside the try block. The control always passes through finally before actually leaving the method.

java
public static void methodOne() {
    try {
        System.out.println("Inside try block");
        return; // Even this return does not skip finally
    } finally {
        System.out.println("Inside finally"); // This still prints
    }
}

Output:

Inside try block
Inside finally

You can only have one finally block per try statement. You can have many catch blocks, but only one finally.

The primary use of finally is cleanup. In Java you often open resources like file streams, database connections, or network sockets. If you open a stream inside a try block and an exception fires before you close it, the stream stays open and leaks resources. The finally block guarantees cleanup happens regardless:

java
public static void readFile() throws Exception {
    SomeResource resource = openResource();
    try {
        // Use the resource
        resource.read();
    } catch (Exception e) {
        System.out.println("Error while reading: " + e.getMessage());
    } finally {
        // This runs whether reading succeeded or failed
        resource.close(); // Always clean up
        System.out.println("Resource closed");
    }
}

Another common use is logging. Since finally always executes, you can place a log statement there and be certain it runs every time the method is exited, regardless of the exit path.

finally does not run only when the program itself stops before reaching it: the process is killed or the machine shuts down, the JVM crashes, or System.exit() is called inside the try block. Errors such as OutOfMemoryError and StackOverflowError are thrown like any other Throwable, so the finally blocks on the way up still run. Outside of these cases, finally always runs.

You can also use try with finally but without any catch block at all. This is valid when you want to ensure cleanup but you still want any exception to propagate up to the caller:

java
// try-finally without catch: cleans up but lets exception propagate
public static void processData() throws ClassNotFoundException {
    try {
        methodOne(); // Might throw ClassNotFoundException
    } finally {
        System.out.println("Cleanup always happens here");
        // Exception still propagates to whoever called processData
    }
}

Custom Exception Classes ​

Java's built in exceptions cover a lot of ground, but sometimes you need an exception that carries meaning specific to your domain. You can create your own exception classes by extending Exception or any of its subclasses.

If you extend Exception directly, your custom exception behaves like a checked exception and the compiler will require callers to handle it. If you extend RuntimeException, it behaves like an unchecked exception and callers can choose whether to handle it.

java
// Custom checked exception: extends Exception
public class InsufficientFundsException extends Exception {

    public InsufficientFundsException(String message) {
        super(message); // Pass the message to the parent Exception class
    }
}
java
// Using the custom exception
public class BankAccount {

    private double balance;

    public BankAccount(double initialBalance) {
        this.balance = initialBalance;
    }

    public void withdraw(double amount) throws InsufficientFundsException {
        if (amount > balance) {
            throw new InsufficientFundsException(
                "Attempted to withdraw " + amount + " but balance is only " + balance
            );
        }
        balance -= amount;
    }
}

public class Main {
    public static void main(String[] args) {
        BankAccount account = new BankAccount(100.0);
        try {
            account.withdraw(200.0);
        } catch (InsufficientFundsException e) {
            System.out.println("Transaction failed: " + e.getMessage());
        }
    }
}

Because InsufficientFundsException extends Exception, it behaves exactly like any other checked exception. The compiler requires callers of withdraw to either handle it with try and catch or declare that they also throw it. You place it in the exception hierarchy where it makes sense. If you want a domain specific exception that is more specific than a general Exception but not a runtime exception, extending Exception is the right choice.

You can also extend a specific built in exception if your custom exception is a specialized version of something that already exists:

java
// Custom exception that is a more specific kind of IOException
public class DatabaseConnectionException extends IOException {
    public DatabaseConnectionException(String message) {
        super(message);
    }
}

Why Exception Handling Makes Your Code Better ​

Now that you know how exception handling works, you should understand why it exists. The clearest way to see the benefit is to compare the same problem solved with and without exceptions.

Suppose you have a method that takes a class number (1 through 12), looks up how many students are in that class, creates an array for them, and fills in the first slot. There are several things that could go wrong: the class number might be invalid, the capacity might be zero, the array might be null. Without exception handling, you have to guard every step with an if statement and return error codes:

java
// Without exception handling: messy, hard to read
public static int processClass(int classNumber) {
    if (classNumber <= 0 || classNumber > 12) {
        return -1; // Error code for invalid class number
    }

    int numberOfStudents = getStudentCapacity(classNumber);
    if (numberOfStudents == 0) {
        return -2; // Error code for empty class
    }

    String[] names = new String[numberOfStudents];
    if (names == null || names.length == 0) {
        return -3; // Error code for array problem
    }

    names[0] = "New Student";
    return 0; // Success
}

Now the caller receives an integer and has to check it: if it is minus 1 do this, if it is minus 2 do that. And if this method is called by another method which is called by another method, that error code has to be passed all the way up the chain. Every method in the chain has to check the return value and cascade it upward.

With exception handling, the same logic becomes far cleaner:

java
// With exception handling: clean, readable, focused
public static void processClass(int classNumber) throws IllegalArgumentException, IOException {
    try {
        // Your actual business logic, uncluttered by error checks
        int numberOfStudents = getStudentCapacity(classNumber);
        String[] names = new String[numberOfStudents];
        names[0] = "New Student";
    } catch (ArrayIndexOutOfBoundsException e) {
        System.out.println("Array problem: " + e.getMessage());
    } catch (Exception e) {
        System.out.println("Unexpected error: " + e.getMessage());
    }
}

The business logic is in the try block. The error handling is in the catch blocks. They are separated. When you read the try block you see what the method is supposed to do, not a maze of validation checks. When something goes wrong, the exception object carries rich information: the type, the message, the exact line number, the full stack trace. You do not have to invent your own error code system.

Instead of cascading an integer error code up through every layer of the call stack, you can let the exception propagate automatically using throws. The exception object travels up the call chain on its own. Any layer that wants to handle it can. Any layer that does not can declare throws and stay out of it.

Exception handling also gives you a chance to recover. When you catch an exception, you can sometimes correct the situation and continue. If an index is out of bounds, maybe you can resize. If a file is not found, maybe you can create it or use a default. You are not forced to terminate.

From a debugging standpoint, the stack trace is invaluable. Without exception handling, if you return error code minus 10 from deep in a call chain, you know something went wrong but you have no idea where or why. With an exception, you have the exact file, the exact line, the exact method chain, and a message. Debugging becomes dramatically faster.

Exceptions also improve security. When a failure occurs, you control exactly what information appears in logs. You can catch an exception, log a safe sanitized message, and avoid leaking sensitive data like passwords or customer personal information into log files.

The Cost of Exception Handling ​

Exception handling is not free. There is one disadvantage you should understand. When an exception propagates up a long call chain without being caught, the JVM has to walk that entire chain checking at each level. If your call stack is 100 methods deep and none of them handle the exception, that is 100 checks before the program terminates. That is overhead.

This cost is usually acceptable because exceptions represent genuinely exceptional situations, not normal code flow. But it means you should not use exceptions as a substitute for simple conditional checks that you could handle with an if statement.

Consider this:

java
// Using exception handling for something a simple if would handle
public static int divide(int a, int b) {
    try {
        return a / b;
    } catch (ArithmeticException e) {
        return -1;
    }
}

// Simpler and cheaper: just check the condition
public static int divide(int a, int b) {
    if (b == 0) {
        return -1;
    }
    return a / b;
}

Both work. But the second version has no overhead. There is no exception object created, no stack unwinding. If you can handle a predictable condition with a simple check, do that. Reserve exception handling for situations that are genuinely exceptional, not just conditions you can test for with an if.

Check yourself ​

  1. What does an exception object contain, and what does the runtime do with the stack trace?
  2. What is the difference between Error and Exception? Is Error checked or unchecked?
  3. What is the difference between checked and unchecked exceptions? Give two examples of each.
  4. What is the difference between throw and throws? Why would you catch an exception only to rethrow it?
  5. In what order must catch blocks be written, and when would you use a multi-catch?
  6. When does finally run, and when does it not?
  7. How do you create a custom checked exception, and a custom unchecked one?
  8. Why should a simple if check be preferred over catching an exception when possible?
Answers
  1. The exception type, a message, and the stack trace. The runtime walks back up the call chain looking for a handler; if none is found, the program terminates and prints the stack trace.
  2. Error is a JVM-level problem outside your code's control (OutOfMemoryError, StackOverflowError) that you should not try to handle; Exception is a problem your code can handle. Error is unchecked.
  3. Checked exceptions must be caught or declared with throws, or the code does not compile (IOException, ClassNotFoundException). Unchecked ones extend RuntimeException and are not checked by the compiler (NullPointerException, ArithmeticException).
  4. throw actually throws an exception object; throws in a method signature declares that the method may throw it and passes responsibility to the caller. Rethrowing lets a layer do its own work (such as logging) before passing the exception up.
  5. Most specific first, then more general; a general catch (Exception e) first makes the later ones unreachable (compile error). Multi-catch (catch (A | B e)) is for types that need the same handling.
  6. After the try and any catch, on every exit path including return and thrown errors. It does not run if the process is killed, the JVM crashes, or System.exit() is called first.
  7. Extend Exception for a checked exception; extend RuntimeException for an unchecked one.
  8. Throwing and propagating an exception has overhead (creating the object and unwinding the call stack), so exceptions should be kept for genuinely exceptional situations.