DEV Community

Cover image for Exceptions in Java
Adesola
Adesola

Posted on

Exceptions in Java

Exceptions in Java, Explained

Before we get to exceptions specifically, it helps to zoom out and look at the three kinds of errors you'll run into as a Java developer.

Three kinds of errors

Compile-time errors — usually caused by bad syntax. The code won't even build.
Logical errors — the code compiles and runs fine, but produces the wrong result because the logic is flawed, not the syntax.
Runtime errors — the code compiles fine, but something goes wrong while it's actually running, and execution breaks.

A classic example of a runtime problem: your application depends on a file, the file gets deleted from the server, and the moment your code tries to read it — crash. This is exactly the kind of situation exceptions exist to handle, so your application can respond gracefully instead of dying mid-execution.

Handling exceptions with try/catch

Arithmetic exception

public class StoreApplication {
    public static void main(String[] args) {
        int i = 45;
        int j = 0;

        try {
            int output = i / j;
            System.out.println("The result of the division: " + output);
        } catch (Exception e) {
            System.out.println("Execution could have stopped here due to this error: \"" + e.getMessage() + "\"");
        }

        System.out.println("Execution continues.");
    }
}
Enter fullscreen mode Exit fullscreen mode

Array index out of bounds

public class StoreApplication {
    public static void main(String[] args) {
        int[] nums = new int[5]; // valid indices: 0 to 4

        try {
            System.out.println("array value at position one: " + nums[1]);
            System.out.println("array value at position five: " + nums[5]); // out of bounds
        } catch (Exception e) {
            System.out.println("Execution could have stopped here due to this error: \"" + e.getMessage() + "\"");
        }

        System.out.println("Execution continues.");
    }
}
Enter fullscreen mode Exit fullscreen mode

Multiple catch blocks

You can catch different exception types separately, which lets you respond differently depending on what actually went wrong:

public class StoreApplication {
    public static void main(String[] args) {
        int i = 45;
        int j = 4;
        int[] nums = new int[5];

        try {
            int output = i / j;
            System.out.println("array value at position one: " + nums[1]);
            System.out.println("array value at position five: " + nums[5]);
            System.out.println("The result of the division: " + output);
        } catch (ArithmeticException e) {
            System.out.println("Arithmetic exception caught: \"" + e.getMessage() + "\"");
        } catch (IndexOutOfBoundsException e) {
            System.out.println("Index out of bounds exception caught: \"" + e.getMessage() + "\"");
        } catch (Exception e) {
            System.out.println("Some other exception caught: \"" + e.getMessage() + "\"");
        }

        System.out.println("Execution continues.");
    }
}
Enter fullscreen mode Exit fullscreen mode

Important ordering rule: catch blocks are checked top to bottom, and only the first match runs. If you put the general Exception catch before the more specific ones, the specific catch blocks become unreachable — the compiler will actually flag this as an error, because Exception would already catch everything below it:

This won't compile — unreachable catch blocks

try {
    // ...
} catch (Exception e) {
    // this catches everything first
} catch (ArithmeticException e) {
    // unreachable
} catch (IndexOutOfBoundsException e) {
    // unreachable
}
Enter fullscreen mode Exit fullscreen mode

The rule of thumb: most specific exception first, most general last.

Multi-catch shorthand

If two exception types should be handled identically, you don't need two blocks — Java lets you combine them with |:

try {
    // risky code
} catch (ArithmeticException | IndexOutOfBoundsException e) {
    System.out.println("Caught a numeric or bounds error: " + e.getMessage());
}
Enter fullscreen mode Exit fullscreen mode

The Throwable hierarchy

Every error and exception in Java is a subclass of Throwable, which sits directly under Object. Throwable is a class, not an interface — it's unrelated to marker/functional interfaces like Cloneable, Runnable, or Serializable, even though the naming pattern looks similar at a glance.

Object
  └── Throwable
        ├── Error
        │     ├── IOError
        │     ├── ThreadDeath
        │     └── VirtualMachineError (e.g. OutOfMemoryError)
        │
        └── Exception
              ├── RuntimeException
              │     ├── ArithmeticException
              │     ├── IndexOutOfBoundsException
              │     │     └── ArrayIndexOutOfBoundsException
              │     └── NullPointerException
              │
              ├── SQLException
              └── IOException
Enter fullscreen mode Exit fullscreen mode

Two branches matter most in day-to-day code:

Errors — things you generally can't recover from (like running out of memory). You typically don't catch these; when they happen, execution simply stops.
Exceptions — things you can and should handle, so your program keeps running.

Checked vs. unchecked exceptions

Unchecked exceptions are RuntimeException and everything under it (ArithmeticException, NullPointerException, etc.). The compiler doesn't force you to catch or declare them.

Checked exceptions are everything else under Exception (IOException, SQLException, etc.). The compiler does force you to either catch them or declare them with throws.

This leads us into a keyword distinction that's easy to mix up.

throw vs. throws

These look similar but do completely different jobs:

throw actually throws an exception instance, at the point where something goes wrong:

if (balance < amount) {
    throw new IllegalArgumentException("Insufficient balance");
}
Enter fullscreen mode Exit fullscreen mode

throws goes in a method signature to declare that the method might throw a checked exception, so callers know they need to handle it:

public void readFile(String path) throws IOException {
    // ...
}

Enter fullscreen mode Exit fullscreen mode

The finally block

A finally block runs whether or not an exception was thrown — it's the place for cleanup code (closing a connection, releasing a resource) that absolutely must happen either way:

try {
    int output = 45 / 0;
} catch (ArithmeticException e) {
    System.out.println("Caught: " + e.getMessage());
} finally {
    System.out.println("This always runs, exception or not.");
}
Enter fullscreen mode Exit fullscreen mode

try-with-resources

For anything that needs closing (files, streams, connections), modern Java offers try-with-resources, which closes the resource automatically — no finally block needed:

try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) {
    System.out.println(reader.readLine());
} catch (IOException e) {
    System.out.println("Could not read file: " + e.getMessage());
}
Enter fullscreen mode Exit fullscreen mode

Custom exceptions

Sometimes the built-in exceptions don't describe your problem well enough, and a custom exception communicates intent much more clearly than a generic one. To create one, extend Exception (for a checked exception) and pass the message up to the parent via super:

class InsufficientBalanceException extends Exception {
    public InsufficientBalanceException(String message) {
        super(message);
    }
}


public class BankAccount {
    private double balance = 100.0;

    public void withdraw(double amount) throws InsufficientBalanceException {
        if (amount > balance) {
            throw new InsufficientBalanceException("Cannot withdraw " + amount + ", balance is only " + balance);
        }
        balance -= amount;
    }

    public static void main(String[] args) {
        BankAccount account = new BankAccount();
        try {
            account.withdraw(150.0);
        } catch (InsufficientBalanceException e) {
            System.out.println("Transaction failed: " + e.getMessage());
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

If you want an unchecked custom exception instead — the one callers aren't forced to catch — extend RuntimeException instead of Exception, following the same pattern.

Wrapping up

Exceptions exist so your program can fail safely instead of failing silently or crashing outright. The real skill isn't just wrapping code in try/catch — it's knowing which exceptions are worth catching specifically, which ones should bubble up, and when a custom exception communicates your intent better than a generic one.

What's a tricky exception-handling bug you've run into? Share it in the comments.

Top comments (0)