Lesson 18 — Handling Errors

Exception Handling

Real software encounters unexpected conditions: files that don't exist, invalid user input, network failures, division by zero. Exception handling lets your program respond gracefully instead of crashing.

The Exception Hierarchy

In Java, every exception is an object. The hierarchy matters:

  • Throwable — root of all throwable types.
  • Error — serious JVM problems (OutOfMemoryError, StackOverflowError) — don't try to catch these.
  • Exception — recoverable problems — these are what you handle.
  • RuntimeException — subclass of Exception — unchecked: NullPointerException, ArrayIndexOutOfBoundsException, IllegalArgumentException.
  • All other Exception subclasses — checked: IOException, SQLException — the compiler forces you to handle or declare these.

try / catch / finally

JAVA — try / catch / finally
public class FileProcessor {
    public static void main(String[] args) {
        try {
            // Risky code goes here
            int[] marks = {65, 72, 80};
            System.out.println(marks[5]); // index 5 doesn't exist

        } catch (ArrayIndexOutOfBoundsException e) {
            // Handles this specific exception type
            System.out.println("Error: " + e.getMessage());
            System.out.println("Valid indexes are 0 to " + (marks.length - 1));

        } finally {
            // Runs ALWAYS — whether or not an exception occurred
            // Ideal for releasing resources: closing files, DB connections
            System.out.println("Processing attempt complete.");
        }
    }
}

Multiple catch Blocks

Catch different exception types in separate blocks. Order matters — catch more specific types first; a broader type (like Exception) must come after all specific ones.

JAVA — Multiple catches
public static double safeDivide(String numerator, String denominator) {
    try {
        int n = Integer.parseInt(numerator);    // may throw NumberFormatException
        int d = Integer.parseInt(denominator);   // may throw NumberFormatException
        return (double) n / d;                   // may throw ArithmeticException

    } catch (NumberFormatException e) {
        System.out.println("Non-numeric input: " + e.getMessage());
        return 0.0;

    } catch (ArithmeticException e) {
        System.out.println("Division by zero.");
        return Double.NaN;

    } catch (Exception e) {
        // Catch-all for anything unexpected — rarely ideal
        System.out.println("Unexpected: " + e.getClass().getSimpleName());
        return 0.0;
    }
}

System.out.println(safeDivide("10",  "4"));   // 2.5
System.out.println(safeDivide("10",  "0"));   // Division by zero. NaN
System.out.println(safeDivide("abc", "4"));   // Non-numeric input. 0.0

Checked vs Unchecked Exceptions

JAVA — Checked vs unchecked
import java.io.*;

// Checked exception — compiler forces you to handle it
public static String readFirstLine(String filename) throws IOException {
    // If you don't handle IOException here, you must declare throws IOException
    try (BufferedReader reader = new BufferedReader(new FileReader(filename))) {
        return reader.readLine(); // may throw IOException
    }
}

// Unchecked exception — no compile-time requirement, but you should still handle it
public static int getElement(int[] arr, int index) {
    if (index < 0 || index >= arr.length) {
        // Throw unchecked — caller should validate before calling
        throw new IllegalArgumentException(
            "Index " + index + " is out of range for array of length " + arr.length);
    }
    return arr[index];
}

Creating Custom Exceptions

Custom exceptions make your code self-documenting — instead of a generic IllegalArgumentException, throw an InsufficientFundsException that tells the reader exactly what went wrong.

JAVA — Custom exception
// Custom checked exception
public class InsufficientFundsException extends Exception {
    private double shortfall;

    public InsufficientFundsException(double shortfall) {
        super(String.format("Insufficient funds. Short by R%.2f.", shortfall));
        this.shortfall = shortfall;
    }

    public double getShortfall() { return shortfall; }
}

// Using it in BankAccount:
public void withdraw(double amount) throws InsufficientFundsException {
    if (amount > balance) {
        throw new InsufficientFundsException(amount - balance);
    }
    balance -= amount;
}

// Handling it:
try {
    account.withdraw(5000.00);
} catch (InsufficientFundsException e) {
    System.out.println(e.getMessage());
    System.out.printf("You need an additional R%.2f.%n", e.getShortfall());
}

try-with-resources

Ensures resources (files, database connections, network sockets) are automatically closed even if an exception occurs. Any class implementing AutoCloseable can be used.

JAVA
// Without try-with-resources (risky — close might not be called on exception):
BufferedReader reader = null;
try {
    reader = new BufferedReader(new FileReader("data.txt"));
    System.out.println(reader.readLine());
} catch (IOException e) {
    System.out.println("Error: " + e.getMessage());
} finally {
    if (reader != null) try { reader.close(); } catch (IOException e) { }
}

// With try-with-resources (clean — close is always called):
try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) {
    System.out.println(reader.readLine());
} catch (IOException e) {
    System.out.println("Error: " + e.getMessage());
}try-with-resources

Practice Task

Your Turn

Write a LearnerDatabase class that stores learners in an array. Add methods: addLearner(Learner l) throws DatabaseFullException (custom) if the array is full, getLearner(int index) throws LearnerNotFoundException (custom) if the index is invalid, updateMark(int index, double mark) throws InvalidMarkException if mark is outside 0-100. Test in main with all valid and invalid cases, catching each exception and printing a meaningful message.

Common Mistakes

  • Empty catch blocks — catching and doing nothing hides bugs completely. At minimum, log the exception.
  • Catching Exception everywhere — catch the most specific type you can actually handle.
  • Using exceptions for normal flow control — exceptions are for exceptional conditions, not expected logic paths.
  • Forgetting finally for resource cleanup — use try-with-resources for anything that needs closing.
  • Throwing checked exceptions where unchecked would be more appropriate — if the caller cannot reasonably recover, use RuntimeException or a subclass.

Professional Tip

Print the stack trace during development: e.printStackTrace() shows you exactly where the exception was thrown and the call chain leading to it. In production, log it properly instead of printing to the console.

Mini Quiz

What is the difference between checked and unchecked exceptions?