Your program reads a file that might not exist. Without exception handling, it crashes. With try-catch, you can detect the error and respond gracefully - show a message, try a different file, or use default data.

Basic try-catch

Catch and handle an exception.

BasicTryCatch.java
// Basic Try-Catch: Division by Zero

public class BasicTryCatch {
    public static void main(String[] args) {
        System.out.println("=== Basic Try-Catch ===\n");

        // Without try-catch - would crash
        // int result = 10 / 0;  // ArithmeticException!

        // With try-catch - handle gracefully
        int numerator = 10;
        int denominator = ;

        try {
            System.out.println("Attempting division...");
            int result = numerator / denominator;
            System.out.println("Result: " + result);
        } catch (ArithmeticException e) {
            System.out.println("Error caught!");
            System.out.println("Exception type: " + e.getClass().getName());
            System.out.println("Message: " + e.getMessage());
        }

        System.out.println("\nProgram continues normally...");

        // Demonstrate successful division
        System.out.println("\n--- Successful Division ---");

        int a = 20;
        int b = 4;

        try {
            int result = a / b;
            System.out.println(a + " / " + b + " = " + result);
        } catch (ArithmeticException e) {
            System.out.println("Error: " + e.getMessage());
        }

        // Multiple attempts in one try block
        System.out.println("\n--- Multiple Operations ---");

        int[] numbers = {10, 5, 0, 2};
        int dividend = 100;

        for (int num : numbers) {
            try {
                int result = dividend / num;
                System.out.println(dividend + " / " + num + " = " + result);
            } catch (ArithmeticException e) {
                System.out.println(dividend + " / " + num + " = Error (division by zero)");
            }
        }

        System.out.println("\n=== Key Points ===");
        System.out.println("""
            1. try block contains risky code
            2. catch block handles the exception
            3. Specify exception type to catch
            4. e.getMessage() gives error details
            5. Program continues after catch block
            """);
    }
}

//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
// Basic Try-Catch: Division by Zero

public class BasicTryCatch {
    public static void main(String[] args) {
        System.out.println("=== Basic Try-Catch ===\n");

        // Without try-catch - would crash
        // int result = 10 / 0;  // ArithmeticException!

        // With try-catch - handle gracefully
        int numerator = 10;
        int denominator = ;

        try {
            System.out.println("Attempting division...");
            int result = numerator / denominator;
            System.out.println("Result: " + result);
        } catch (ArithmeticException e) {
            System.out.println("Error caught!");
            System.out.println("Exception type: " + e.getClass().getName());
            System.out.println("Message: " + e.getMessage());
        }

        System.out.println("\nProgram continues normally...");

        // Demonstrate successful division
        System.out.println("\n--- Successful Division ---");

        int a = 20;
        int b = 4;

        try {
            int result = a / b;
            System.out.println(a + " / " + b + " = " + result);
        } catch (ArithmeticException e) {
            System.out.println("Error: " + e.getMessage());
        }

        // Multiple attempts in one try block
        System.out.println("\n--- Multiple Operations ---");

        int[] numbers = {10, 5, 0, 2};
        int dividend = 100;

        for (int num : numbers) {
            try {
                int result = dividend / num;
                System.out.println(dividend + " / " + num + " = " + result);
            } catch (ArithmeticException e) {
                System.out.println(dividend + " / " + num + " = Error (division by zero)");
            }
        }

        System.out.println("\n=== Key Points ===");
        System.out.println("""
            1. try block contains risky code
            2. catch block handles the exception
            3. Specify exception type to catch
            4. e.getMessage() gives error details
            5. Program continues after catch block
            """);
    }
}

//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//

Code that might fail goes in try. Error handling goes in catch.

try-catch `try { risky code } catch (Exception e) { handle it }`. Prevents crashes.

Catch array bounds exception

Handle accessing invalid array indices.

ArrayBounds.java
// Array Index Out of Bounds Exception

public class ArrayBounds {
    public static void main(String[] args) {
        System.out.println("=== Array Bounds Exception ===\n");

        // Create an array
        String[] fruits = {"Apple", "Banana", "Cherry"};
        System.out.println("Array: [Apple, Banana, Cherry]");
        System.out.println("Valid indices: 0, 1, 2");
        System.out.println("Length: " + fruits.length);

        // Access valid index
        System.out.println("\n--- Valid Access ---");
        try {
            String fruit = fruits[1];
            System.out.println("fruits[1] = " + fruit);
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("Error: " + e.getMessage());
        }

        // Access invalid index (too high)
        System.out.println("\n--- Invalid Access (index too high) ---");
        try {
            String fruit = fruits[5];
            System.out.println("fruits[5] = " + fruit);
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("Error: Index 5 out of bounds");
            System.out.println("Message: " + e.getMessage());
        }

        // Access invalid index (negative)
        System.out.println("\n--- Invalid Access (negative index) ---");
        try {
            String fruit = fruits[-1];
            System.out.println("fruits[-1] = " + fruit);
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("Error: Negative index");
            System.out.println("Message: " + e.getMessage());
        }

        // Safe access pattern
        System.out.println("\n--- Safe Access Pattern ---");
        int[] indicesToTry = {0, 2, 5, -1, 1};

        for (int index : indicesToTry) {
            try {
                String fruit = fruits[index];
                System.out.println("fruits[" + index + "] = " + fruit);
            } catch (ArrayIndexOutOfBoundsException e) {
                System.out.println("fruits[" + index + "] = Invalid index!");
            }
        }

        // Alternative: Check before accessing
        System.out.println("\n--- Check Before Access ---");
        int requestedIndex = ;

        if (requestedIndex >= 0 && requestedIndex < fruits.length) {
            System.out.println("fruits[" + requestedIndex + "] = " + fruits[requestedIndex]);
        } else {
            System.out.println("Index " + requestedIndex + " is out of bounds (0-" + (fruits.length - 1) + ")");
        }

        System.out.println("\n=== Key Points ===");
        System.out.println("""
            1. Arrays have fixed size (0 to length-1)
            2. Invalid index throws ArrayIndexOutOfBoundsException
            3. Both too high AND negative indices are invalid
            4. Can catch and handle, or check bounds first
            5. getMessage() shows the invalid index number
            """);
    }
}

//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
// Array Index Out of Bounds Exception

public class ArrayBounds {
    public static void main(String[] args) {
        System.out.println("=== Array Bounds Exception ===\n");

        // Create an array
        String[] fruits = {"Apple", "Banana", "Cherry"};
        System.out.println("Array: [Apple, Banana, Cherry]");
        System.out.println("Valid indices: 0, 1, 2");
        System.out.println("Length: " + fruits.length);

        // Access valid index
        System.out.println("\n--- Valid Access ---");
        try {
            String fruit = fruits[1];
            System.out.println("fruits[1] = " + fruit);
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("Error: " + e.getMessage());
        }

        // Access invalid index (too high)
        System.out.println("\n--- Invalid Access (index too high) ---");
        try {
            String fruit = fruits[5];
            System.out.println("fruits[5] = " + fruit);
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("Error: Index 5 out of bounds");
            System.out.println("Message: " + e.getMessage());
        }

        // Access invalid index (negative)
        System.out.println("\n--- Invalid Access (negative index) ---");
        try {
            String fruit = fruits[-1];
            System.out.println("fruits[-1] = " + fruit);
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("Error: Negative index");
            System.out.println("Message: " + e.getMessage());
        }

        // Safe access pattern
        System.out.println("\n--- Safe Access Pattern ---");
        int[] indicesToTry = {0, 2, 5, -1, 1};

        for (int index : indicesToTry) {
            try {
                String fruit = fruits[index];
                System.out.println("fruits[" + index + "] = " + fruit);
            } catch (ArrayIndexOutOfBoundsException e) {
                System.out.println("fruits[" + index + "] = Invalid index!");
            }
        }

        // Alternative: Check before accessing
        System.out.println("\n--- Check Before Access ---");
        int requestedIndex = ;

        if (requestedIndex >= 0 && requestedIndex < fruits.length) {
            System.out.println("fruits[" + requestedIndex + "] = " + fruits[requestedIndex]);
        } else {
            System.out.println("Index " + requestedIndex + " is out of bounds (0-" + (fruits.length - 1) + ")");
        }

        System.out.println("\n=== Key Points ===");
        System.out.println("""
            1. Arrays have fixed size (0 to length-1)
            2. Invalid index throws ArrayIndexOutOfBoundsException
            3. Both too high AND negative indices are invalid
            4. Can catch and handle, or check bounds first
            5. getMessage() shows the invalid index number
            """);
    }
}

//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
// Array Index Out of Bounds Exception

public class ArrayBounds {
    public static void main(String[] args) {
        System.out.println("=== Array Bounds Exception ===\n");

        // Create an array
        String[] fruits = {"Apple", "Banana", "Cherry"};
        System.out.println("Array: [Apple, Banana, Cherry]");
        System.out.println("Valid indices: 0, 1, 2");
        System.out.println("Length: " + fruits.length);

        // Access valid index
        System.out.println("\n--- Valid Access ---");
        try {
            String fruit = fruits[1];
            System.out.println("fruits[1] = " + fruit);
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("Error: " + e.getMessage());
        }

        // Access invalid index (too high)
        System.out.println("\n--- Invalid Access (index too high) ---");
        try {
            String fruit = fruits[5];
            System.out.println("fruits[5] = " + fruit);
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("Error: Index 5 out of bounds");
            System.out.println("Message: " + e.getMessage());
        }

        // Access invalid index (negative)
        System.out.println("\n--- Invalid Access (negative index) ---");
        try {
            String fruit = fruits[-1];
            System.out.println("fruits[-1] = " + fruit);
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("Error: Negative index");
            System.out.println("Message: " + e.getMessage());
        }

        // Safe access pattern
        System.out.println("\n--- Safe Access Pattern ---");
        int[] indicesToTry = {0, 2, 5, -1, 1};

        for (int index : indicesToTry) {
            try {
                String fruit = fruits[index];
                System.out.println("fruits[" + index + "] = " + fruit);
            } catch (ArrayIndexOutOfBoundsException e) {
                System.out.println("fruits[" + index + "] = Invalid index!");
            }
        }

        // Alternative: Check before accessing
        System.out.println("\n--- Check Before Access ---");
        int requestedIndex = ;

        if (requestedIndex >= 0 && requestedIndex < fruits.length) {
            System.out.println("fruits[" + requestedIndex + "] = " + fruits[requestedIndex]);
        } else {
            System.out.println("Index " + requestedIndex + " is out of bounds (0-" + (fruits.length - 1) + ")");
        }

        System.out.println("\n=== Key Points ===");
        System.out.println("""
            1. Arrays have fixed size (0 to length-1)
            2. Invalid index throws ArrayIndexOutOfBoundsException
            3. Both too high AND negative indices are invalid
            4. Can catch and handle, or check bounds first
            5. getMessage() shows the invalid index number
            """);
    }
}

//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//

ArrayIndexOutOfBoundsException when accessing index outside array range.

Multiple catch blocks

Handle different exceptions differently.

MultipleCatch.java
// Multiple Catch Blocks

public class MultipleCatch {
    public static void main(String[] args) {
        System.out.println("=== Multiple Catch Blocks ===\n");

        // Different exceptions need different handling
        String[] data = {"10", "abc", "20", null, "30"};
        int[] divisors = {2, 5, 0, 4, 1};

        System.out.println("Processing data with multiple exception types...\n");

        for (int i = 0; i < data.length; i++) {
            System.out.println("--- Processing index " + i + " ---");

            try {
                // Step 1: Parse string to int (may throw NumberFormatException)
                String str = data[i];
                System.out.println("String value: " + str);
                int number = Integer.parseInt(str);
                System.out.println("Parsed number: " + number);

                // Step 2: Divide (may throw ArithmeticException)
                int divisor = divisors[i];
                int result = number / divisor;
                System.out.println("Result: " + number + " / " + divisor + " = " + result);

            } catch (NumberFormatException e) {
                System.out.println("ERROR: Cannot parse '" + data[i] + "' as number");
                System.out.println("  Type: NumberFormatException");

            } catch (ArithmeticException e) {
                System.out.println("ERROR: Division by zero");
                System.out.println("  Type: ArithmeticException");

            } catch (NullPointerException e) {
                System.out.println("ERROR: Null value encountered");
                System.out.println("  Type: NullPointerException");
            }

            System.out.println();
        }

        // Multi-catch syntax (Java 7+)
        System.out.println("=== Multi-Catch Syntax ===\n");

        String[] testValues = {"100", "bad", null};
        int testDivisor = 10;

        for (String val : testValues) {
            try {
                int num = Integer.parseInt(val);
                int result = num / testDivisor;
                System.out.println(val + " / " + testDivisor + " = " + result);

            } catch (NumberFormatException | NullPointerException e) {
                // Handle both the same way
                System.out.println("Invalid input: " + val);
                System.out.println("  Exception: " + e.getClass().getSimpleName());
            }
        }

        // Order matters - specific before general
        System.out.println("\n=== Catch Order (Specific First) ===\n");

        demonstrateCatchOrder();

        System.out.println("=== Key Points ===");
        System.out.println("""
            1. Multiple catch blocks handle different exceptions
            2. Order: specific exceptions before general ones
            3. Multi-catch: catch (TypeA | TypeB e) for same handling
            4. Only ONE catch block executes per exception
            5. Put most specific exception types first
            """);
    }

    static void demonstrateCatchOrder() {
        String value = "not a number";

        try {
            int num = Integer.parseInt(value);
            System.out.println("Parsed: " + num);

        } catch (NumberFormatException e) {
            // Specific exception - caught first
            System.out.println("Caught NumberFormatException (specific)");

        } catch (IllegalArgumentException e) {
            // Parent of NumberFormatException - never reached for NFE
            System.out.println("Caught IllegalArgumentException (parent)");

        } catch (Exception e) {
            // Most general - catches anything else
            System.out.println("Caught Exception (general)");
        }

        System.out.println("\nNote: NumberFormatException extends IllegalArgumentException");
        System.out.println("So the specific catch must come first!");
    }
}

//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//

Specific exceptions first, general exception last. Order matters.

multi-catch `catch (A | B e)` handles multiple types. Or use separate catch blocks.

Exception hierarchy

Catch parent type to handle all subtypes.

ExceptionHierarchy.java
// Exception Hierarchy

public class ExceptionHierarchy {
    public static void main(String[] args) {
        System.out.println("=== Exception Hierarchy ===\n");

        // Exception inheritance tree
        System.out.println("Java Exception Hierarchy:");
        System.out.println("""
            Throwable
            ├── Error (serious, don't catch)
            │   ├── OutOfMemoryError
            │   └── StackOverflowError
            └── Exception (catch these)
                ├── RuntimeException (unchecked)
                │   ├── NullPointerException
                │   ├── ArithmeticException
                │   ├── IndexOutOfBoundsException
                │   │   └── ArrayIndexOutOfBoundsException
                │   ├── IllegalArgumentException
                │   │   └── NumberFormatException
                │   └── ClassCastException
                └── IOException (checked)
                    └── FileNotFoundException
            """);

        // Catching parent catches all children
        System.out.println("--- Parent Exception Catches Children ---\n");

        Object[] testCases = {
            "divide_zero",   // ArithmeticException
            "null_pointer",  // NullPointerException
            "bad_index",     // ArrayIndexOutOfBoundsException
            "bad_parse"      // NumberFormatException
        };

        for (Object testCase : testCases) {
            System.out.println("Test: " + testCase);

            try {
                triggerException((String)testCase);
            } catch (RuntimeException e) {
                // RuntimeException catches ALL runtime exceptions
                System.out.println("  Caught: " + e.getClass().getSimpleName());
                System.out.println("  Message: " + e.getMessage());
            }
            System.out.println();
        }

        // Catching Exception catches everything
        System.out.println("--- Catching Exception (Most General) ---\n");

        try {
            triggerException("null_pointer");
        } catch (Exception e) {
            System.out.println("Exception catches any exception type");
            System.out.println("Actual type: " + e.getClass().getName());
        }

        // instanceof check for specific handling
        System.out.println("\n--- instanceof for Specific Handling ---\n");

        for (Object testCase : testCases) {
            try {
                triggerException((String)testCase);
            } catch (Exception e) {
                handleWithInstanceof(e);
            }
        }

        // Why use specific exceptions?
        System.out.println("\n=== Why Catch Specific Exceptions? ===");
        System.out.println("""
            1. Different errors need different recovery
            2. Don't hide unexpected errors
            3. Better error messages for users
            4. Easier debugging
            5. Code is self-documenting
            """);
    }

    static void triggerException(String type) {
        switch (type) {
            case "divide_zero" -> {
                int result = 10 / 0;
            }
            case "null_pointer" -> {
                String s = null;
                s.length();
            }
            case "bad_index" -> {
                int[] arr = {1, 2, 3};
                int val = arr[10];
            }
            case "bad_parse" -> {
                int num = Integer.parseInt("abc");
            }
        }
    }

    static void handleWithInstanceof(Exception e) {
        System.out.print("Handling: ");

        if (e instanceof ArithmeticException) {
            System.out.println("Math error - check your calculations");
        } else if (e instanceof NullPointerException) {
            System.out.println("Null error - check for null values");
        } else if (e instanceof ArrayIndexOutOfBoundsException) {
            System.out.println("Index error - check array bounds");
        } else if (e instanceof NumberFormatException) {
            System.out.println("Parse error - check input format");
        } else {
            System.out.println("Unknown error: " + e.getClass().getSimpleName());
        }
    }
}

//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//

Catching Exception catches everything. Be specific when possible.

Finally block

Code that always runs, even after exception.

FinallyBlock.java
// Finally Block

public class FinallyBlock {
    public static void main(String[] args) {
        System.out.println("=== Finally Block ===\n");

        // Finally always executes
        System.out.println("--- Finally with Exception ---");

        try {
            System.out.println("1. In try block");
            int result = 10 / 0;
            System.out.println("2. This won't print");
        } catch (ArithmeticException e) {
            System.out.println("3. In catch block");
        } finally {
            System.out.println("4. In finally block (ALWAYS runs)");
        }

        System.out.println("5. After try-catch-finally");

        // Finally without exception
        System.out.println("\n--- Finally without Exception ---");

        try {
            System.out.println("1. In try block");
            int result = 10 / 2;
            System.out.println("2. Result: " + result);
        } catch (ArithmeticException e) {
            System.out.println("3. In catch block (SKIPPED)");
        } finally {
            System.out.println("4. In finally block (ALWAYS runs)");
        }

        System.out.println("5. After try-catch-finally");

        // Finally for cleanup
        System.out.println("\n--- Finally for Cleanup ---");

        demonstrateCleanup(true);
        System.out.println();
        demonstrateCleanup(false);

        // Finally with return
        System.out.println("\n--- Finally with Return ---");

        int result1 = calculateWithReturn(10, 2);
        System.out.println("Result: " + result1);

        int result2 = calculateWithReturn(10, 0);
        System.out.println("Result: " + result2);

        // Try-finally without catch
        System.out.println("\n--- Try-Finally (no catch) ---");

        try {
            System.out.println("Performing operation...");
            // Operations here
        } finally {
            System.out.println("Cleanup runs even without catch block");
        }

        System.out.println("\n=== Key Points ===");
        System.out.println("""
            1. finally ALWAYS executes
            2. Executes after try OR after catch
            3. Use for cleanup: close files, release resources
            4. finally runs even if return in try/catch
            5. Can have try-finally without catch
            """);
    }

    static void demonstrateCleanup(boolean success) {
        System.out.println("Starting operation (success=" + success + ")");

        // Simulating resource acquisition
        System.out.println("  [RESOURCE] Acquired");

        try {
            if (success) {
                System.out.println("  [OPERATION] Success");
            } else {
                System.out.println("  [OPERATION] About to fail...");
                throw new RuntimeException("Operation failed");
            }
        } catch (RuntimeException e) {
            System.out.println("  [ERROR] " + e.getMessage());
        } finally {
            // Always release the resource
            System.out.println("  [RESOURCE] Released (finally)");
        }
    }

    static int calculateWithReturn(int a, int b) {
        try {
            int result = a / b;
            System.out.println("  Calculation successful");
            return result;
        } catch (ArithmeticException e) {
            System.out.println("  Calculation failed");
            return -1;
        } finally {
            // This runs BEFORE the return!
            System.out.println("  Finally block executed");
        }
    }
}

//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//

finally { cleanup } runs whether exception occurred or not.

finally Cleanup code that always runs. Close resources, release locks.

Exercise: Practical.java

Build a robust file reader with complete exception handling