⚡IntermediateAdvanced Exception Handling

Multi-Catch and Exception Chaining

Catch multiple exception types in one catch block, and chain exceptions to preserve the original cause.

Overview

Java 7 introduced multi-catch: you can catch multiple exception types in a single catch block using the | operator. This avoids duplicating the same handler for different exceptions. Exception chaining (also called exception wrapping) lets you capture the original exception as the cause of a new exception. Use new SomeException(message, originalException) to preserve context when translating exceptions across layers. Together, these features lead to cleaner, more informative exception handling code.

Key Points

  • •catch (IOException | SQLException e) catches either type in one block
  • •In a multi-catch, the variable e is implicitly final
  • •The types in a multi-catch must not be related by inheritance
  • •Exception chaining: throw new MyException("msg", originalEx)
  • •getCause() retrieves the original exception; the full chain is in the stack trace
  • •Rethrowing: catch (Exception e) { throw e; } re-throws without wrapping
  • •catch (Exception e) { throw new RuntimeException(e); } wraps a checked in unchecked

Examples

Multi-catch block

import java.io.IOException;
import java.sql.SQLException;

public class Main {
    static void riskyOperation(int choice) throws IOException, SQLException {
        if (choice == 1) throw new IOException("File not found");
        if (choice == 2) throw new SQLException("Connection lost");
        System.out.println("Success for choice " + choice);
    }

    public static void main(String[] args) {
        for (int i = 0; i <= 3; i++) {
            try {
                riskyOperation(i);
            } catch (IOException | SQLException e) {
                // One handler for both types
                System.out.println("Handled " + e.getClass().getSimpleName() + ": " + e.getMessage());
            }
        }
    }
}

Multi-catch avoids duplicating the same handling logic. The variable e is treated as the common supertype of the listed exceptions, so only methods defined on that supertype are available.

Exception chaining across layers

public class Main {
    static class ServiceException extends RuntimeException {
        ServiceException(String msg, Throwable cause) { super(msg, cause); }
    }

    static int parseId(String s) {
        try {
            return Integer.parseInt(s);
        } catch (NumberFormatException e) {
            // Translate low-level parsing exception to domain exception
            throw new ServiceException("Invalid user ID format: " + s, e);
        }
    }

    static String findUser(String rawId) {
        int id = parseId(rawId);
        if (id != 42) throw new ServiceException("User not found: " + id, null);
        return "Alice";
    }

    public static void main(String[] args) {
        String[] ids = {"42", "abc", "99"};
        for (String id : ids) {
            try {
                System.out.println(findUser(id));
            } catch (ServiceException e) {
                System.out.println("Service error: " + e.getMessage());
                if (e.getCause() != null) {
                    System.out.println("  Caused by: " + e.getCause().getMessage());
                }
            }
        }
    }
}

getCause() traverses the chain. The original NumberFormatException is preserved inside the ServiceException, so you can log the root cause while showing a clean domain-level message to callers.

Rethrowing and wrapping checked as unchecked

import java.io.IOException;
import java.util.List;

public class Main {
    static List<String> readLines(String path) {
        try {
            // Simulated: would use Files.readAllLines(Path.of(path))
            if (path.equals("missing.txt")) throw new IOException("File missing");
            return List.of("line1", "line2");
        } catch (IOException e) {
            // Wrap checked IOException in unchecked to avoid polluting method signature
            throw new RuntimeException("Cannot read file: " + path, e);
        }
    }

    public static void main(String[] args) {
        try {
            List<String> lines = readLines("missing.txt");
            lines.forEach(System.out::println);
        } catch (RuntimeException e) {
            System.out.println("Error: " + e.getMessage());
            System.out.println("Root cause: " + e.getCause().getClass().getSimpleName());
        }

        // Normal case
        readLines("data.txt").forEach(System.out::println);
    }
}

Wrapping a checked exception in RuntimeException is a common pattern in functional code (streams, lambdas) because lambda bodies cannot throw checked exceptions. Always include the original as the cause.

Common Mistakes

  • Listing related types in a multi-catch: catch (Exception | IOException e) is a compile error because IOException is a subtype of Exception. Just catch Exception
  • Reassigning the multi-catch variable: since it is implicitly final, you cannot do e = new IOException(). This would cause a compile error
  • Wrapping and losing the cause: throw new RuntimeException("msg") without passing the original exception. The original stack trace is lost, making debugging very hard

Note

Use multi-catch when two or more exceptions require identical handling. If the handling differs even slightly, use separate catch blocks. Exception chaining is mandatory when translating between layers: always pass the original exception as the cause.

Exercises

Write Java code and click Run to execute it instantly. When your output looks correct, mark as done.

Loading editor…

Completed: 0 / 4 exercises

Knowledge Check

Question 1 of 4

Which multi-catch declaration is invalid?