Week 2

Inheritance

Interfaces

Polymorphism

Error Handling in Java

Practice

Assignment

Back end Track

🎯 Learning Objectives

By the end of this module, you will be able to:


1. What are Exceptions?

An exception is an event that disrupts the normal flow of a program at runtime. When something goes wrong — dividing by zero, accessing a null reference, reading a file that doesn't exist — Java creates an exception object that describes the problem and immediately stops executing the current code path.

Without any handling, an exception crashes the program and prints a stack trace — the chain of method calls that led to the problem:

public class Main {
    public static void main(String[] args) {
        int result = divide(10, 0);
        System.out.println(result); // never reached
    }

    public static int divide(int a, int b) {
        return a / b;
    }
}
Exception in thread "main" java.lang.ArithmeticException: / by zero
    at Main.divide(Main.java:7)
    at Main.main(Main.java:3)

Reading a stack trace: start from the top (where the exception occurred) and work down (how it got there). Main.divide line 7 is where division by zero happened; Main.main line 3 is what called it.

🔍 JavaScript has try/catch too, and exceptions work the same way conceptually. The key difference is that Java distinguishes between exceptions the compiler forces you to handle and those it doesn't — a distinction JavaScript doesn't make.


2. The Exception Hierarchy

Every exception in Java is an object. They all sit in a class hierarchy rooted at Throwable:

Throwable
├── Error                          ← JVM-level problems, do NOT catch these
│   ├── OutOfMemoryError
│   ├── StackOverflowError
│   └── ...
└── Exception                      ← Problems your program should handle
    ├── IOException                ← Checked
    ├── SQLException               ← Checked
    ├── FileNotFoundException      ← Checked
    └── RuntimeException           ← Unchecked
        ├── NullPointerException
        ├── ArithmeticException
        ├── ArrayIndexOutOfBoundsException
        ├── IllegalArgumentException
        ├── IllegalStateException
        └── ...

Error — represents serious JVM-level failures your program cannot recover from (OutOfMemoryError, StackOverflowError). Never catch these — if the JVM runs out of memory, there is nothing your application can do.

Exception — the branch your code deals with. Split into two families with very different rules.


3. Checked vs Unchecked Exceptions

This is the most important distinction in Java exception handling.

Unchecked Exceptions (RuntimeException and subclasses)

These indicate programming errors — bugs in your logic that should be fixed, not caught. The compiler does not require you to handle them.

String name = null;
name.length(); // NullPointerException — you should have checked for null

int[] arr = new int[3];
arr[5] = 10;   // ArrayIndexOutOfBoundsException — index out of range

int x = 10 / 0; // ArithmeticException — divide by zero

The right response to unchecked exceptions is usually to fix the code, not wrap it in a try-catch.

Checked Exceptions (Exception subclasses excluding RuntimeException)

These represent external conditions outside your control — a file that doesn't exist, a network that drops, a database that's unavailable. The compiler forces you to handle them. If you don't, the code won't compile.

// Reading a file — FileNotFoundException is checked
public void readFile(String path) {
    FileReader reader = new FileReader(path); // ❌ compile error: unhandled exception
}

// You must either handle it...
public void readFile(String path) {
    try {
        FileReader reader = new FileReader(path);
    } catch (FileNotFoundException e) {
        System.out.println("File not found: " + path);
    }
}

// ...or declare that you pass it up the call stack
public void readFile(String path) throws FileNotFoundException {
    FileReader reader = new FileReader(path);
}
Unchecked Checked
Extends RuntimeException Exception (not via RuntimeException)
Compiler enforcement ❌ None ✅ Must handle or declare
Represents Programming bugs External failures
Correct response Fix the code Handle gracefully
Examples NullPointerException, ArithmeticException IOException, SQLException

4. try-catch Blocks

Wrap risky code in a try block. If an exception occurs, execution jumps immediately to the matching catch block. Code after the throw point inside try is skipped.

try {
    int result = divide(10, 0);
    System.out.println(result); // skipped if exception occurs above
} catch (ArithmeticException e) {
    System.out.println("Cannot divide by zero: " + e.getMessage());
}
// program continues here normally

Catching Multiple Exceptions

Catch each exception type separately to handle them differently:

try {
    String input = getUserInput();
    int number   = Integer.parseInt(input);  // NumberFormatException if not a number
    int result   = 100 / number;             // ArithmeticException if zero
    System.out.println("Result: " + result);
} catch (NumberFormatException e) {
    System.out.println("Please enter a valid number.");
} catch (ArithmeticException e) {
    System.out.println("Number cannot be zero.");
}

Multi-catch (Java 7+)

When two exceptions need the same handling, combine them with |:

try {
    riskyOperation();
} catch (IOException | SQLException e) {
    System.out.println("Data operation failed: " + e.getMessage());
}

The Exception Variable

Inside the catch block, e is the exception object. Use it to get information about what went wrong:

catch (IOException e) {
    System.out.println(e.getMessage());    // human-readable description
    e.printStackTrace();                   // full stack trace — useful for debugging
}

⚠️ e.printStackTrace() is useful during development but should not be the sole error handling in production code. Always log or communicate the error meaningfully.


5. The finally Block

Code inside finally always runs — whether an exception was thrown or not, whether it was caught or not. This makes it the right place for cleanup code that must always execute.

Scanner scanner = new Scanner(System.in);

try {
    System.out.print("Enter a number: ");
    int number = scanner.nextInt();
    System.out.println("Square: " + (number * number));
} catch (InputMismatchException e) {
    System.out.println("That was not a valid number.");
} finally {
    scanner.close(); // always runs — resource is always cleaned up
    System.out.println("Scanner closed.");
}

finally runs in all three scenarios:

Scenario try body catch body finally body
No exception ✅ Runs fully ❌ Skipped ✅ Runs
Exception caught ⚠️ Stops at throw ✅ Runs ✅ Runs
Exception not caught ⚠️ Stops at throw ❌ Skipped ✅ Runs before crash

💡 The main use cases for finally are closing file handles, releasing database connections, and unlocking resources. In modern Java, try-with-resources (next section) handles most of these more cleanly.


6. try-with-resources — Automatic Resource Cleanup

try-with-resources is a cleaner syntax for managing resources that must be closed after use — files, database connections, network streams. Any resource that implements the AutoCloseable interface can be used here.

Declare the resource inside the parentheses after try. Java automatically calls .close() on it when the block exits — even if an exception is thrown.

// Without try-with-resources — verbose, easy to forget close()
FileReader reader = null;
try {
    reader = new FileReader("data.txt");
    // ... read file
} catch (IOException e) {
    System.out.println("Error: " + e.getMessage());
} finally {
    if (reader != null) {
        try { reader.close(); } catch (IOException e) { /* ignore */ }
    }
}

// With try-with-resources — clean, automatic
try (FileReader reader = new FileReader("data.txt")) {
    // ... read file
    // reader.close() called automatically when block exits
} catch (IOException e) {
    System.out.println("Error: " + e.getMessage());
}

Multiple resources can be declared separated by a semicolon:

try (FileReader reader  = new FileReader("input.txt");
     FileWriter writer  = new FileWriter("output.txt")) {
    // both closed automatically
} catch (IOException e) {
    System.out.println("File operation failed: " + e.getMessage());
}

💡 You will use try-with-resources extensively when working with databases and file I/O in later weeks. For now, understand the pattern and why it exists — it eliminates an entire class of resource-leak bugs.


7. throw — Throwing Exceptions Explicitly

You can throw an exception yourself using the throw keyword. This is how you signal to the caller that something has gone wrong — typically when input validation fails or a precondition is violated.

public static double calculateSquareRoot(double number) {
    if (number < 0) {
        throw new IllegalArgumentException(
            "Cannot calculate square root of a negative number: " + number
        );
    }
    return Math.sqrt(number);
}
System.out.println(calculateSquareRoot(25.0));  // 5.0
System.out.println(calculateSquareRoot(-4.0));  // throws IllegalArgumentException

💡 Always throw the most specific exception type that fits the situation. IllegalArgumentException is for invalid input values. IllegalStateException is for calling a method when the object is in the wrong state. NullPointerException can be thrown explicitly when a required argument is null — though the Objects utility class has Objects.requireNonNull() for this.


8. throws — Declaring Exceptions in Method Signatures

When a method can throw a checked exception but doesn't handle it internally, it must declare this in its signature using throws. This tells callers: "if you call this method, you must deal with this exception."