Skip to main content
    Courses/Java/Exception Handling

    Lesson 12 • Advanced

    Exception Handling

    In the real world things go wrong — files vanish, input is garbage, networks drop. Learn to catch those failures and keep your program running instead of crashing.

    What You'll Learn in This Lesson

    • You'll be able to wrap risky code in try / catch / finally
    • You'll know the Throwable / Error / Exception hierarchy
    • You'll tell checked from unchecked exceptions and pick the right one
    • You'll raise errors with throw and declare them with throws
    • You'll handle several error types with multi-catch
    • You'll auto-close resources with try-with-resources and write custom exceptions

    📚 Before You Start

    This lesson builds on a few earlier ones. You should be comfortable with:

    Real-World Analogy

    An exception is like a fire alarm in a building. When something goes badly wrong, you don't want everyone to freeze and the building to collapse — you want an emergency procedure that takes over.

    throw = pulling the alarm: "something is wrong, stop normal work now."

    try = the part of the building wired to the alarm system.

    catch = the trained response: evacuate, call for help, recover.

    finally = locking up afterwards — it happens whether or not there was a fire.

    Without this, one bad input would crash the whole program. With it, you detect the problem, respond, and carry on.

    1️⃣ try / catch / finally — the basic shape

    You put risky code — anything that might fail — inside a try block. If it throws an exception, Java jumps straight to a matching catch block and runs your recovery code. The finally block runs afterwards no matter what, so it's where cleanup goes.

    try {
        // Risky code that might fail
        int result = 10 / 0;             // throws ArithmeticException
        System.out.println("Never runs");
    } catch (ArithmeticException e) {
        // Runs only if that specific error happened
        System.out.println("Oops: " + e.getMessage());
    } finally {
        // ALWAYS runs — perfect for cleanup
        System.out.println("Cleanup complete");
    }

    Every exception object carries a message you can read with e.getMessage() and a type you can read with e.getClass().getSimpleName(). The next example uses both.

    2️⃣ The exception hierarchy

    Every error in Java is an object, and they all descend from one root class, Throwable. The shape of this tree decides what you must catch versus what's optional:

    Throwable
    ├── Error                  (DON'T catch — JVM is in trouble)
    │   ├── OutOfMemoryError
    │   └── StackOverflowError
    └── Exception              (your problems live here)
        ├── IOException        (CHECKED   — compiler forces handling)
        ├── SQLException       (CHECKED   — compiler forces handling)
        └── RuntimeException   (UNCHECKED — optional to catch)
            ├── NullPointerException
            ├── ArrayIndexOutOfBoundsException
            ├── ArithmeticException
            └── IllegalArgumentException

    Error means the JVM itself is broken (out of memory, infinite recursion). You normally let these crash — there's nothing useful you can do.

    Checked exceptions extend Exception but not RuntimeException. The compiler refuses to build until you handle them. They model recoverable situations a caller should plan for.

    Unchecked exceptions extend RuntimeException. They usually mean a bug in your code — a null you forgot to check, an index off the end of an array.

    Worked Example: try / catch / finally and multi-catch
    public class Main {
        // throw raises an exception; this method may raise ArithmeticException.
        static double divide(int a, int b) {
            if (b == 0) throw new ArithmeticException("Cannot divide by zero");
            return (double) a / b;        // safe — b is non-zero here
        }
    
        public static void main(String[] args) {
            System.out.println("1. Happy path:");
            try {
                System.out.println("  Result: " + divide(10, 2));   // prints 5.0
            } catch (ArithmeticException e) {
                System.out.println("  Caught: " + e.getMessage());  // skipped
            } finally {
                System.out.println("  Finally always runs");        // runs
            }
    
            System.out.println("2. Error path:");
            try {
                divide(10, 0);                          // throws here
                System.out.println("  Never prints!");  // skipped — jumped to catch
            } catch (ArithmeticException e) {
                System.out.println("  Caught: " + e.getMessage());   // Cannot divide by zero
            } finally {
                System.out.println("  Finally STILL runs");          // runs even on error
            }
    
            System.out.println("3. Multi-catch (one block, many types):");
            Object[] inputs = { "42", "oops", null };
            for (Object in : inputs) {
                try {
                    int n = Integer.parseInt((String) in);  // may throw 2 different types
                    System.out.println("  Parsed " + n);
                } catch (NumberFormatException | NullPointerException e) {
                    // | means "catch EITHER of these the same way"
                    System.out.println("  Bad input: " + e.getClass().getSimpleName());
                }
            }
        }
    }
    Output
    1. Happy path:
      Result: 5.0
      Finally always runs
    2. Error path:
      Caught: Cannot divide by zero
      Finally STILL runs
    3. Multi-catch (one block, many types):
      Parsed 42
      Bad input: NumberFormatException
      Bad input: NullPointerException
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    3️⃣ throw vs throws (two different words!)

    throw (no s) is a statement — it actually raises an exception right now. throws (with s) is part of a method signature — it warns callers that this method might raise a checked exception.

    // 'throw' raises an exception to reject bad input
    void setAge(int age) {
        if (age < 0 || age > 150) {
            throw new IllegalArgumentException("Age must be 0-150, got: " + age);
        }
        this.age = age;
    }
    
    // 'throws' declares a CHECKED exception so the compiler warns callers
    void readFile(String path) throws IOException {
        // ... code that can throw IOException ...
    }

    4️⃣ Multi-catch — one block, several types

    When two different exceptions need the same handling, don't write two near-identical catch blocks. Join the types with a single pipe |:

    try {
        process(userInput);
    } catch (NumberFormatException | NullPointerException e) {
        // Handles EITHER type the same way
        System.out.println("Bad input: " + e.getMessage());
    }

    Order still matters when types are related: a more specific type must come before its parent, or you'll get a compile error for unreachable code.

    5️⃣ Custom exceptions — name your failures

    A custom exception is just a class that extends an existing exception. Extend Exception for a checked one (callers must handle it) or RuntimeException for an unchecked one. The payoff: a name that says exactly what went wrong, plus room for extra data.

    // Checked — callers MUST handle or declare it
    class InsufficientFundsException extends Exception {
        private final double deficit;
        public InsufficientFundsException(double deficit) {
            super("Short by $" + deficit);   // pass a message up to Exception
            this.deficit = deficit;          // store extra context
        }
        public double getDeficit() { return deficit; }
    }

    Now a caller can write catch (InsufficientFundsException e) and read e.getDeficit() — far clearer than catching a generic RuntimeException and guessing.

    Worked Example: custom exceptions, throws, and checked vs unchecked
    // CHECKED custom exception — extends Exception, so callers MUST handle or declare it.
    class InsufficientFundsException extends Exception {
        private final double deficit;
        public InsufficientFundsException(double balance, double amount) {
            super("Need $" + String.format("%.2f", amount - balance) + " more");
            this.deficit = amount - balance;   // extra data the caller can read
        }
        public double getDeficit() { return deficit; }
    }
    
    // UNCHECKED custom exception — extends RuntimeException, so no throws clause needed.
    class AccountFrozenException extends RuntimeException {
        public AccountFrozenException(String reason) { super("Account frozen: " + reason); }
    }
    
    class BankAccount {
        private double balance;
        private boolean frozen = false;
        private String freezeReason;
        BankAccount(double initial) { this.balance = initial; }
    
        void freeze(String reason) { frozen = true; freezeReason = reason; }
    
        // 'throws InsufficientFundsException' declares the CHECKED exception to callers.
        void withdraw(double amount) throws InsufficientFundsException {
            if (frozen) throw new AccountFrozenException(freezeReason);          // unchecked
            if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
            if (amount > balance) throw new InsufficientFundsException(balance, amount);
            balance -= amount;
        }
        double getBalance() { return balance; }
    }
    
    public class Main {
        public static void main(String[] args) {
            BankAccount acct = new BankAccount(100);
    
            try {
                acct.withdraw(30);
                System.out.println("Withdrew $30, balance: $" + acct.getBalance());  // $70.0
            } catch (InsufficientFundsException e) {
                System.out.println(e.getMessage());
            }
    
            try {
                acct.withdraw(500);   // too much — throws checked exception
            } catch (InsufficientFundsException e) {
                // Read the custom field for context
                System.out.println(e.getMessage() + " (deficit $" + String.format("%.2f", e.getDeficit()) + ")");
            }
    
            acct.freeze("suspicious activity");
            try {
                acct.withdraw(50);    // throws UNCHECKED AccountFrozenException
            } catch (AccountFrozenException e) {        // caught even though not declared
                System.out.println(e.getMessage());
            } catch (InsufficientFundsException e) {
                System.out.println(e.getMessage());
            }
        }
    }
    Output
    Withdrew $30, balance: $70.0
    Need $430.00 more (deficit $430.00)
    Account frozen: suspicious activity
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    6️⃣ Try-with-resources (Java 7+)

    Analogy: it's a self-closing door — you walk through and it shuts behind you. Declare a resource in the try (...) parentheses and Java calls its close() method automatically when the block ends, even if an exception is thrown. The resource just has to implement AutoCloseable.

    // OLD way — verbose and easy to get wrong
    BufferedReader reader = null;
    try {
        reader = new BufferedReader(new FileReader("data.txt"));
        System.out.println(reader.readLine());
    } finally {
        if (reader != null) reader.close();   // you must remember this!
    }
    
    // MODERN way — clean and leak-proof
    try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) {
        System.out.println(reader.readLine());
    }   // reader.close() called AUTOMATICALLY — no finally needed

    You can also chain exceptions: when you catch a low-level error and rethrow a higher-level one, pass the original as the second argument so the cause isn't lost. Read it back with e.getCause(). The next example shows both.

    Worked Example: try-with-resources and exception chaining
    // A class that implements AutoCloseable can be used in try-with-resources.
    class DatabaseConnection implements AutoCloseable {
        private final String name;
        DatabaseConnection(String name) {
            this.name = name;
            System.out.println("  Opened " + name);
        }
        String query(String sql) {
            System.out.println("  Running: " + sql);
            return "2 rows";
        }
        @Override public void close() {     // called AUTOMATICALLY at the end of the block
            System.out.println("  Closed " + name);
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            System.out.println("1. Resource auto-cleanup:");
            // Resource declared in (...) is closed for you — even if an exception is thrown.
            try (DatabaseConnection conn = new DatabaseConnection("UserDB")) {
                System.out.println("  Got: " + conn.query("SELECT * FROM users"));
            }   // conn.close() runs here, no finally needed
    
            System.out.println("2. Exception chaining (keep the original cause):");
            try {
                try {
                    throw new NumberFormatException("not a number");   // low-level cause
                } catch (NumberFormatException cause) {
                    // Wrap the cause so the stack trace shows BOTH problems.
                    throw new IllegalStateException("Could not load config", cause);
                }
            } catch (IllegalStateException e) {
                System.out.println("  " + e.getMessage());
                System.out.println("  caused by: " + e.getCause().getMessage());  // getCause()
            }
        }
    }
    Output
    1. Resource auto-cleanup:
      Opened UserDB
      Running: SELECT * FROM users
      Got: 2 rows
      Closed UserDB
    2. Exception chaining (keep the original cause):
      Could not load config
      caused by: not a number
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    🎯 Your Turn #1 — try / catch / finally

    Fill in the three blanks so the program catches a bad array index and still runs its cleanup line. The expected output is in the comments — match it exactly.

    Fill in the blanks (___)
    public class Main {
        public static void main(String[] args) {
            // 🎯 YOUR TURN — fill in the blanks marked with ___
            int[] scores = { 90, 80 };
    
            // 1) Wrap the risky array access in a try block
            ___ {                                  // 👉 the keyword that starts protected code
                System.out.println(scores[5]);     // index 5 doesn't exist!
            }
            // 2) Catch the exception thrown by a bad index
            catch (___ e) {                        // 👉 ArrayIndexOutOfBoundsException
                System.out.println("Bad index: " + e.getMessage());
            }
            // 3) Add a block that runs no matter what
            ___ {                                  // 👉 the keyword for guaranteed cleanup
                System.out.println("Done checking scores");
            }
    
            // ✅ Expected output:
            // Bad index: Index 5 out of bounds for length 2
            // Done checking scores
        }
    }
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    🎯 Your Turn #2 — throw a custom checked exception

    Complete the custom exception so it's checked, then raise it with the right keyword. The throws clause and the catch are already wired up for you.

    Fill in the blanks (___)
    public class Main {
        // A custom CHECKED exception
        static class TooYoungException extends ___ {     // 👉 extend this to make it CHECKED
            TooYoungException(int age) { super("Age " + age + " is under 18"); }
        }
    
        // Declares it may throw the checked exception
        static void checkAge(int age) throws TooYoungException {
            if (age < 18) {
                ___ new TooYoungException(age);          // 👉 the keyword that raises it
            }
            System.out.println(age + " is old enough");
        }
    
        public static void main(String[] args) {
            try {
                checkAge(21);
                checkAge(15);
            } catch (TooYoungException e) {
                System.out.println("Rejected: " + e.getMessage());
            }
    
            // ✅ Expected output:
            // 21 is old enough
            // Rejected: Age 15 is under 18
        }
    }
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    🧩 Mini-Challenge — Safe config loader

    Now with the scaffolding removed. Read the brief in the comments and write the method yourself — only the outline is given. Use a try / catch (NumberFormatException e) to fall back to a default when the text isn't a number.

    Write it yourself — outline only
    public class Main {
        // 🎯 MINI-CHALLENGE: Safe configuration loader
        //
        // 1. Write a method:  static int parsePort(String text)
        //    - Use Integer.parseInt(text) to turn the text into an int
        //    - Wrap it in try / catch (NumberFormatException e)
        //    - On a bad value, print "Invalid port, using 8080" and RETURN 8080
        //    - On success, return the parsed number
        // 2. In main, call it with "9090" and with "abc" and print each result.
        //
        // ✅ Expected output:
        // Port: 9090
        // Invalid port, using 8080
        // Port: 8080
    
        public static void main(String[] args) {
            // your code here
        }
    }
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    Common Errors (and how to fix them)

    ❌ Swallowing the exception (empty catch)

    catch (Exception e) { }   // error vanishes — you'll never know it happened

    Fix: at minimum log it, or rethrow it. A silent failure is the hardest bug to find: catch (IOException e) { log.error("read failed", e); }

    ❌ Catching Exception too broadly

    catch (Exception e) { ... }   // also traps NullPointerException and other bugs!

    Fix: catch the most specific type you can actually recover from, and let real bugs propagate: catch (FileNotFoundException e) before catch (IOException e).

    ❌ Leaking resources without try-with-resources

    var conn = openConnection();
    conn.use();
    conn.close();   // SKIPPED if use() throws — connection leaks!

    Fix: declare the resource in try (...) so close() always runs: try (var conn = openConnection()) { conn.use(); }

    ❌ Confusing checked and unchecked

    // error: unreported exception IOException; must be caught or declared

    Fix: that message means a checked exception escaped unhandled. Either wrap the call in try/catch or add throws IOException to the method. Unchecked (RuntimeException) types never produce this error.

    ❌ Using exceptions for normal flow control

    Don't replace an if with a try/catch. Exceptions are far slower than a plain conditional and they hide your logic — reserve them for genuinely exceptional situations.

    📋 Quick Reference

    Keyword / ideaSyntaxPurpose
    trytry { risky }Wrap code that might fail
    catchcatch (IOException e)Handle one specific error
    finallyfinally { cleanup }Always runs — cleanup
    throwthrow new Ex("msg")Raise an exception now
    throwsvoid f() throws IOExceptionDeclare a checked exception
    multi-catchcatch (A | B e)One block, several types
    try-with-resourcestry (var r = open())Auto-close AutoCloseable
    checkedextends ExceptionCompiler forces handling
    uncheckedextends RuntimeExceptionOptional to catch (bugs)

    ❓ Frequently Asked Questions

    What is the difference between a checked and an unchecked exception?

    A checked exception extends Exception (but not RuntimeException) and the compiler forces every caller to either catch it or declare it with throws — it models recoverable conditions a caller should plan for, like a missing file. An unchecked exception extends RuntimeException and needs no throws clause; it usually signals a programming bug such as a null reference or a bad array index. Rule of thumb: checked for expected, recoverable problems; unchecked for bugs.

    Does the finally block always run?

    Almost always. finally runs whether the try block finishes normally, throws an exception, or even hits a return statement, which makes it the right place for cleanup. The only ways to skip it are extreme: calling System.exit(), the JVM crashing, or an infinite loop. With try-with-resources you rarely need finally at all, because the resource is closed for you.

    When should I create a custom exception instead of reusing a built-in one?

    Create a custom exception when your code has a domain-specific failure that a generic type cannot describe clearly. InsufficientFundsException tells the reader exactly what went wrong and can carry extra data like the deficit, whereas catching a bare RuntimeException tells them nothing. Extend Exception for a checked exception callers must handle, or RuntimeException for an unchecked one.

    Why is catching Exception (or Throwable) considered bad practice?

    catch (Exception e) swallows everything — including bugs like NullPointerException you never meant to handle — so real problems get hidden and your program limps on in a broken state. Catch the most specific type you can actually recover from, and let the rest propagate so they surface. Never catch Throwable, because that also traps Errors like OutOfMemoryError that you cannot meaningfully recover from.

    What does try-with-resources actually do?

    Any object you declare in the try (...) parentheses must implement AutoCloseable, and Java calls its close() method automatically when the block ends — whether it ends normally or via an exception. This replaces the old, error-prone pattern of a null check and close() in a finally block, and it guarantees you never leak files, sockets, or database connections.

    🎉 Lesson Complete!

    You can now write robust Java that survives the unexpected — wrap risky code in try / catch / finally, read the Throwable hierarchy, tell checked from unchecked, raise errors with throw and declare them with throws, collapse handlers with multi-catch, auto-close resources with try-with-resources, and design custom exceptions that name your failures.

    Next up: Collections Framework — work with ArrayList, HashMap, HashSet, and the data structures real programs are built on.

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service