Skip to content
beginnerPhase 12 · Java Exceptions

Exception Handling

Understand the exception hierarchy, try-catch-finally, and throw/throws.

45m
2 problems
Topic Progress0%

Exception Hierarchy

Exception Hierarchy

Java's exception handling is built on a class hierarchy rooted at Throwable. Every exception and error in Java is a subclass of Throwable. This hierarchy is critical to understand because it determines what you can catch and how the JVM treats different failure types.

The two main branches of the hierarchy are:

  • Error: Represents serious system-level problems that applications should not try to catch. These include OutOfMemoryError, StackOverflowError, and VirtualMachineError. These are typically thrown by the JVM and indicate conditions that a reasonable application should not try to catch.

  • Exception: Represents conditions that a reasonable application might want to catch. This is the branch you work with most often. Exception itself has two important subclasses:

    • Checked exceptions (subclasses of Exception but not RuntimeException): Must be declared in method signatures or caught. Examples: IOException, SQLException.
    • Unchecked exceptions (subclasses of RuntimeException): Do not need to be declared or caught. Examples: NullPointerException, ArrayIndexOutOfBoundsException.

Here is a visual representation of the hierarchy:

// The hierarchy in code form:
// Throwable
//   ├── Error
//   │     ├── OutOfMemoryError
//   │     ├── StackOverflowError
//   │     └── AssertionError
//   └── Exception
//         ├── IOException (checked)
//         ├── SQLException (checked)
//         ├── RuntimeException (unchecked)
//         │     ├── NullPointerException
//         │     ├── ArrayIndexOutOfBoundsException
//         │     ├── IllegalArgumentException
//         │     └── ArithmeticException
//         └── ... other checked exceptions

public class ExceptionHierarchyDemo {
    public static void main(String[] args) {
        // Demonstrate that all exceptions are Throwable
        try {
            throw new IOException("File not found");
        } catch (Throwable t) {
            System.out.println("Caught as Throwable: " + t.getMessage());
            System.out.println("Is Exception? " + (t instanceof Exception));
            System.out.println("Is RuntimeException? " + (t instanceof RuntimeException));
        }
    }
}

Understanding this hierarchy helps you decide whether to catch an exception, declare it, or let it propagate. Errors should generally not be caught. Checked exceptions must be handled. Unchecked exceptions often indicate programming bugs and should be fixed at the source rather than caught generically.

Try-Catch-Finally

Try-Catch-Finally

The try-catch-finally construct is the foundation of exception handling in Java. The try block wraps code that might throw an exception. The catch block handles specific exception types. The finally block executes regardless of whether an exception occurred, making it ideal for cleanup.

Key rules:

  • You must have at least one catch or finally block after a try block.
  • Multiple catch blocks are evaluated in order; the first matching one executes.
  • The finally block runs even if a catch block throws an exception or if the method returns.
  • If no exception is thrown, finally still executes after the try block.
import java.io.FileReader;
import java.io.IOException;

public class TryCatchFinallyDemo {
    public static void main(String[] args) {
        FileReader reader = null;
        try {
            reader = new FileReader("data.txt");
            int data = reader.read();
            System.out.println("First character: " + (char) data);
        } catch (IOException e) {
            System.out.println("Error reading file: " + e.getMessage());
        } finally {
            // Always runs - cleanup code here
            if (reader != null) {
                try {
                    reader.close();
                    System.out.println("File closed in finally");
                } catch (IOException e) {
                    System.out.println("Error closing file");
                }
            }
            System.out.println("Finally block executed");
        }
    }

    // Multiple catch blocks example
    public static void processArray(int[] arr, int index) {
        try {
            int value = arr[index] / index;
            System.out.println("Result: " + value);
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("Invalid index: " + index);
        } catch (ArithmeticException e) {
            System.out.println("Cannot divide by zero");
        } catch (Exception e) {
            System.out.println("Unexpected error: " + e.getMessage());
        } finally {
            System.out.println("Processing complete");
        }
    }

    // Catching the most specific exception first
    public static void demonstrateCatchOrder() {
        try {
            String str = null;
            str.length(); // NullPointerException
        } catch (NullPointerException e) {
            // This catches NullPointerException specifically
            System.out.println("NPE caught: " + e.getMessage());
        } catch (RuntimeException e) {
            // This would NOT catch NPE because it was already caught above
            System.out.println("Runtime exception: " + e.getMessage());
        } catch (Exception e) {
            System.out.println("General exception: " + e.getMessage());
        }
    }
}

The finally block is especially useful for releasing resources like database connections, file handles, and network sockets. However, modern Java provides try-with-resources as a cleaner alternative, covered in a later chapter.

Throw and Throws

Throw and Throws

The throw and throws keywords are essential for exception propagation in Java. While they look similar, they serve fundamentally different purposes.

throw keyword: Used inside a method body to explicitly throw an exception object. It transfers control to the nearest enclosing catch block or propagates up the call stack.

throws keyword: Used in a method declaration to specify which checked exceptions the method might throw. This tells the caller that they must handle or declare these exceptions.

public class ThrowThrowsDemo {

    // throws in method declaration - tells caller this method may throw IOException
    public static void readFile(String path) throws java.io.IOException {
        java.io.FileReader file = new java.io.FileReader(path);
        // ... read file
    }

    // throw creates and throws an exception object
    public static void validateAge(int age) {
        if (age < 0) {
            throw new IllegalArgumentException("Age cannot be negative: " + age);
        }
        if (age < 18) {
            throw new IllegalArgumentException("Must be at least 18 years old");
        }
        System.out.println("Age is valid: " + age);
    }

    // Combining throw and throws
    public static int divide(int a, int b) {
        if (b == 0) {
            throw new ArithmeticException("Division by zero");
        }
        return a / b;
    }

    // Method that both throws and catches
    public static void processUserInput(String input) throws IllegalArgumentException {
        if (input == null || input.isEmpty()) {
            throw new IllegalArgumentException("Input cannot be null or empty");
        }
        // Do processing...
    }

    public static void main(String[] args) {
        // Method throws IllegalArgumentException - unchecked, no need to catch
        validateAge(-5);

        // But we CAN catch it if we want
        try {
            validateAge(-5);
        } catch (IllegalArgumentException e) {
            System.out.println("Validation failed: " + e.getMessage());
        }

        // Checked exceptions MUST be caught or declared
        try {
            readFile("data.txt");
        } catch (java.io.IOException e) {
            System.out.println("Cannot read file: " + e.getMessage());
        }

        // Throwing custom exceptions
        try {
            int result = divide(10, 0);
            System.out.println("Result: " + result);
        } catch (ArithmeticException e) {
            System.out.println("Math error: " + e.getMessage());
        }
    }
}

A common pattern is to catch an exception, add context, and rethrow it. This provides more meaningful error messages while preserving the original stack trace:

public static void transferFunds(int from, int to, double amount) throws BankingException {
    try {
        // perform transfer
    } catch (java.sql.SQLException e) {
        throw new BankingException("Transfer failed", e); // chaining exceptions
    }
}

Remember: you cannot use throw in a throws clause, and you cannot use throws inside a method body. They serve distinct roles in the exception handling mechanism.

Multi-catch

Multi-catch (Java 7+)

Java 7 introduced the multi-catch syntax, which allows you to handle multiple exception types in a single catch block. This reduces code duplication when different exceptions require the same handling logic.

Syntax: Separate exception types with the pipe (|) operator in the catch clause.

import java.io.*;
import java.util.Scanner;

public class MultiCatchDemo {

    // Before Java 7: duplicate catch blocks
    public static void processOld(String path) {
        try {
            Scanner sc = new Scanner(new File(path));
            int value = sc.nextInt();
            int result = 100 / value;
            System.out.println("Result: " + result);
        } catch (FileNotFoundException e) {
            System.out.println("File not found: " + e.getMessage());
        } catch (java.util.InputMismatchException e) {
            System.out.println("Invalid input: " + e.getMessage());
        } catch (ArithmeticException e) {
            System.out.println("Math error: " + e.getMessage());
        }
    }

    // After Java 7: multi-catch reduces duplication
    public static void processNew(String path) {
        try {
            Scanner sc = new Scanner(new File(path));
            int value = sc.nextInt();
            int result = 100 / value;
            System.out.println("Result: " + result);
        } catch (FileNotFoundException | java.util.InputMismatchException | ArithmeticException e) {
            System.out.println("Error occurred: " + e.getMessage());
        }
    }

    // The exception variable is implicitly final in multi-catch
    public static void demonstrateImplicitFinal() {
        try {
            String str = null;
            str.length();
        } catch (NullPointerException | ArrayIndexOutOfBoundsException e) {
            // e cannot be reassigned - it's implicitly final
            // e = new Exception("new"); // COMPILE ERROR
            System.out.println("Exception: " + e.getMessage());
        }
    }

    // Multi-catch with try-with-resources
    public static void processWithResources(String path) {
        try (Scanner sc = new Scanner(new File(path))) {
            while (sc.hasNext()) {
                System.out.println(sc.nextLine());
            }
        } catch (FileNotFoundException | java.util.NoSuchElementException e) {
            System.out.println("Error: " + e.getMessage());
        }
    }

    // Practical example: parsing configuration
    public static int parseConfig(String value) {
        try {
            return Integer.parseInt(value);
        } catch (NumberFormatException | NullPointerException e) {
            System.out.println("Invalid configuration value, using default");
            return 0;
        }
    }

    public static void main(String[] args) {
        processOld("data.txt");
        processNew("data.txt");
        System.out.println("Config: " + parseConfig("abc"));
    }
}

Important notes:

  • Exception types in a multi-catch cannot be related (one cannot be a subclass of another).
  • The caught variable is implicitly final and cannot be reassigned.
  • Use multi-catch when different exceptions need identical handling logic.
  • If handling differs per exception, keep separate catch blocks.

Try-with-Resources

Try-with-Resources (Java 7+)

Try-with-resources is a try statement that declares one or more resources to be automatically closed when the try block finishes. This eliminates the need for verbose finally blocks and prevents resource leaks.

Requirements: The resource class must implement the AutoCloseable interface (or Closeable for I/O streams).

import java.io.*;
import java.sql.*;

public class TryWithResourcesDemo {

    // Basic try-with-resources
    public static String readFirstLine(String path) throws IOException {
        try (BufferedReader br = new BufferedReader(new FileReader(path))) {
            return br.readLine();
        } // br.close() is called automatically here
    }

    // Multiple resources - closed in reverse declaration order
    public static void copyFile(String src, String dest) throws IOException {
        try (FileInputStream in = new FileInputStream(src);
             FileOutputStream out = new FileOutputStream(dest)) {

            byte[] buffer = new byte[1024];
            int len;
            while ((len = in.read(buffer)) > 0) {
                out.write(buffer, 0, len);
            }
        } // out closed first, then in
    }

    // Custom AutoCloseable resource
    static class DatabaseConnection implements AutoCloseable {
        private String name;

        public DatabaseConnection(String name) {
            this.name = name;
            System.out.println("Opening connection: " + name);
        }

        public void query(String sql) {
            System.out.println("Executing: " + sql);
        }

        @Override
        public void close() {
            System.out.println("Closing connection: " + name);
        }
    }

    public static void demonstrateCustomResource() {
        try (DatabaseConnection db = new DatabaseConnection("mydb")) {
            db.query("SELECT * FROM users");
        } // close() called automatically
    }

    // Java 9 enhancement: effectively final variables
    public static void demonstrateJava9Enhancement() throws IOException {
        BufferedReader br1 = new BufferedReader(new FileReader("file1.txt"));
        // In Java 9, you can use previously declared variables
        try (br1) { // No need to redeclare
            System.out.println(br1.readLine());
        }
    }

    // Suppressed exceptions
    public static void demonstrateSuppressedExceptions() {
        try (AutoCloseable resource = new AutoCloseable() {
            @Override
            public void close() throws Exception {
                throw new Exception("Exception from close()");
            }
        }) {
            throw new Exception("Exception from try block");
        } catch (Exception e) {
            System.out.println("Main exception: " + e.getMessage());
            Throwable[] suppressed = e.getSuppressed();
            for (Throwable t : suppressed) {
                System.out.println("Suppressed: " + t.getMessage());
            }
        }
    }

    public static void main(String[] args) throws IOException {
        demonstrateCustomResource();
        demonstrateSuppressedExceptions();
        System.out.println("Read: " + readFirstLine("data.txt"));
    }
}

Key benefits:

  • Automatic resource cleanup even if exceptions occur.
  • Cleaner, more readable code.
  • Suppressed exceptions are preserved (not lost as in manual finally blocks).
  • Resources are closed in reverse order of declaration.

Practice Problems

0/2solved
Safe File Reader

Write a method `readFileContents(String path)` that reads and returns the entire contents of a file as a String. If the file does not exist, return the string "File not found". Use try-with-resources and proper exception handling.

Solution
import java.io.*;

public class SafeFileReader {
    public static String readFileContents(String path) {
        try (BufferedReader br = new BufferedReader(new FileReader(path))) {
            StringBuilder sb = new StringBuilder();
            String line;
            while ((line = br.readLine()) != null) {
                sb.append(line).append("\n");
            }
            return sb.toString().trim();
        } catch (IOException e) {
            return "File not found";
        }
    }
}
Exception Chaining

Write a method `parseInteger(String input)` that tries to parse a string to an integer. If parsing fails, throw a `RuntimeException` with the message "Failed to parse: " followed by the original input, and chain the original `NumberFormatException` as the cause.

Solution
public class ExceptionChaining {
    public static int parseInteger(String input) {
        try {
            return Integer.parseInt(input);
        } catch (NumberFormatException e) {
            throw new RuntimeException("Failed to parse: " + input, e);
        }
    }
}

Quiz

1. What is the parent class of all exceptions and errors in Java?

Question 1 options

2. What happens if a finally block contains a return statement?

Question 2 options

3. In a try-with-resources statement, in what order are resources closed?

Question 3 options

4. What is the primary purpose of Java Exceptions?

Question 4 options

Flashcards

Question

What is the difference between throw and throws?

Answer

throw is used inside a method body to explicitly throw an exception. throws is used in a method declaration to specify which checked exceptions the method may propagate to its caller.

Question

What interface must a class implement to be used in try-with-resources?

Answer

AutoCloseable (or Closeable for I/O streams). The close() method is called automatically when the try block completes.

Question

What is the multi-catch syntax in Java 7+?

Answer

Use the pipe operator | to catch multiple exception types in a single catch block: catch (IOException | SQLException e). The caught variable is implicitly final.

Question

What is Java Exceptions?

Answer

Java Exceptions is a key concept in Java programming.

Question

When to use Java Exceptions?

Answer

Use Java Exceptions when building production systems that require reliability, scalability, and maintainability.

Revision Notes

Key Takeaways

  • 1.Throwable is the root of all exceptions and errors in Java
  • 2.Use try-with-resources instead of manual finally blocks for resource cleanup
  • 3.Always catch the most specific exception type first
  • 4.throw creates an exception; throws declares one in a method signature
  • 5.Multi-catch reduces code duplication for identical exception handling

Interview Tips

  • Explain the difference between Error and Exception: Errors are system-level, unchecked, and should not be caught; Exceptions are application-level and can be caught or declared
  • Know when to catch vs when to declare: catch when you can recover, declare when the caller should handle it
  • Discuss try-with-resources improvements over manual finally blocks, including suppressed exceptions
  • Be ready to explain exception chaining and why preserving the original cause matters for debugging

Cheat Sheet

Java Exceptions Cheat Sheet

Hierarchy

Throwable → Error (don't catch) / Exception (handle these)
Exception → Checked (must declare/catch) / RuntimeException → Unchecked

Key Syntax

try { } catch (Type e) { } finally { }
throw new Exception("msg");
void method() throws IOException { }
catch (A | B e) { } // multi-catch
try (Resource r = new Resource()) { } // try-with-resources

Rules

  • catch blocks evaluated in order (most specific first)
  • finally always executes (even after return)
  • try-with-resources: resources closed in reverse declaration order
  • Implicitly final in multi-catch variable