Useful JavaExtra· 40 min read

Exception Handling in Depth

Go beyond basic try/catch: finally, throwing your own errors, the checked-vs-unchecked split, and writing custom exceptions.

What you will learn

  • Use finally and throw/throws correctly
  • Tell checked from unchecked exceptions
  • Write and throw your own custom exception

A quick recap

You already know try / catch: put risky code in try, handle problems in catch, and the program survives instead of crashing. This lesson completes the picture with the parts professional code relies on every day.

finally: code that always runs

A finally block runs no matter what — whether the try succeeded, an exception was caught, or even if none matched. It is the perfect place for clean-up that must happen either way, like closing a file or a database connection.

finally runs whether or not an exception happened
public class Main {
    public static void main(String[] args) {
        try {
            System.out.println("Opening resource...");
            int x = 10 / 0;             // throws an exception
            System.out.println("This line is skipped.");
        } catch (ArithmeticException e) {
            System.out.println("Caught: " + e.getMessage());
        } finally {
            System.out.println("Closing resource (always runs).");
        }
    }
}

Note: Output: Opening resource... Caught: / by zero Closing resource (always runs). The division threw an exception, so the catch ran and printed the message (e.getMessage() gave the detail "/ by zero"). Then the finally block ran too. Even if there had been no error, finally would still have run — that guaranteed clean-up is its whole purpose.

Throwing your own errors with throw

Sometimes you want to signal a problem — for example, a negative age makes no sense. The keyword throw raises an exception on purpose. The caller then has to deal with it.

throw raises an exception when the input is invalid
public class Main {
    static void setAge(int age) {
        if (age < 0) {
            throw new IllegalArgumentException("Age cannot be negative: " + age);
        }
        System.out.println("Age set to " + age);
    }

    public static void main(String[] args) {
        setAge(30);
        setAge(-5);   // this call throws
        System.out.println("This never prints.");
    }
}

Note: Output: Age set to 30 Exception in thread "main" java.lang.IllegalArgumentException: Age cannot be negative: -5 The first call was fine. The second hit the throw, which raised an exception with our message. Because no try / catch wrapped it, the program stopped there — the last line never ran. Throwing lets a method refuse bad data clearly.

Checked vs unchecked exceptions

Java splits exceptions into two groups, and the difference decides whether you are forced to handle them.

  • Unchecked (like ArithmeticException, NullPointerException) usually come from programming bugs. Java does not force you to catch them — you fix the bug instead.
  • Checked (like IOException when reading a file) are problems Java expects might happen in normal use. Java forces you to either catch them or declare them with throws, or your code will not compile.

The word throws (with an s, on the method line) is a warning label that says "calling this method might raise this exception — be ready". It passes the responsibility up to whoever calls the method.

A checked exception must be declared with throws or caught
import java.io.IOException;

public class Main {
    // "throws IOException" warns callers this might fail
    static void readData() throws IOException {
        throw new IOException("Could not read the file");
    }

    public static void main(String[] args) {
        try {
            readData();
        } catch (IOException e) {
            System.out.println("Handled: " + e.getMessage());
        }
    }
}

Note: Output: Handled: Could not read the file IOException is a checked exception, so readData had to declare throws IOException — without it the file would not compile. The main method then chose to handle it in a try / catch. Checked exceptions force you to plan for failures that are part of normal life, like a missing file.

Writing your own custom exception

For your own programs you can invent meaningful exception types. A custom exception is just a class that extends Exception (checked) or RuntimeException (unchecked). A clear name like InsufficientFundsException makes the error self-explanatory.

A custom exception class and a method that throws it
// 1) Define the custom exception
public class InsufficientFundsException extends Exception {
    public InsufficientFundsException(String message) {
        super(message);   // pass the message up to Exception
    }
}

// 2) Throw it where the rule is broken
public class Account {
    double balance = 100;

    void withdraw(double amount) throws InsufficientFundsException {
        if (amount > balance) {
            throw new InsufficientFundsException("Tried to withdraw " + amount + " but balance is " + balance);
        }
        balance = balance - amount;
        System.out.println("Withdrew " + amount + ", balance now " + balance);
    }
}

Note: Output: (No output yet — this defines our own exception type and a method that uses it. Extending Exception makes it checked, so withdraw declares throws InsufficientFundsException. We catch it next.)

Now we put it to work. Because InsufficientFundsException is checked, the calling code must wrap withdraw in a try / catch — exactly as you would for any built-in checked exception. Here main makes one valid withdrawal and one that breaks the rule, then catches the result:

Catching our own custom exception
public class Main {
    public static void main(String[] args) {
        Account acc = new Account();
        try {
            acc.withdraw(30);     // fine
            acc.withdraw(500);    // too much — throws our custom exception
        } catch (InsufficientFundsException e) {
            System.out.println("Refused: " + e.getMessage());
        }
    }
}

Note: Output: Withdrew 30.0, balance now 70.0 Refused: Tried to withdraw 500.0 but balance is 70.0 The first withdrawal worked. The second broke the rule, so withdraw threw our InsufficientFundsException, and the catch printed its message. A named, custom exception makes the problem crystal clear to anyone reading the code or the logs.

The full toolkit at a glance

KeywordRole
tryWrap code that might fail
catchHandle an exception of a given type
finallyAlways run (clean-up), error or not
throwRaise an exception yourself
throwsDeclare that a method might raise a checked exception

Tip: A modern shortcut for clean-up is try-with-resources: try (Scanner sc = new Scanner(...)) { ... }. Anything opened in those brackets is closed automatically when the block ends, so you often do not even need a finally for closing resources.

Q. What is guaranteed about a finally block?

Answer: finally always executes after the try (and any catch), regardless of whether an exception occurred. That makes it the right place for clean-up like closing files or connections.

✍️ Practice

  1. Write a method that throws an IllegalArgumentException if a price is negative, and call it with a bad value inside a try / catch.
  2. Add a finally block to a try / catch that prints "Done" every time, and confirm it runs in both the success and the error case.

🏠 Homework

  1. Create a custom InvalidAgeException (extends Exception). Write a method that throws it when an age is below 0 or above 130, then call the method in a try / catch with both a valid and an invalid age.
Want to learn this with a mentor?

CodingClave runs guided, project-based training (28-day, 45-day & 6-month batches).

Explore Training →