Java Multiple Catch Blocks

Su Suriya Ravichandran Updated 14 Nov 2026
10 min read ·Lesson 125 of 133

Why One try Needs Several Handlers

A single block of code can fail in more than one way, and each failure usually needs a different response. Reading a number from a file can fail because the file is missing (tell the admin), because the text isn't a number (report the bad value), or because the value is out of range (reject the configuration). One generic handler would have to treat all of these the same way.

Java solves this in two ways:

  1. Several catch blocks after one try, each handling a different exception type differently.
  2. A multi-catch block (Java 7+), where one catch handles several types with the same code, separated by a vertical bar (|).

Several catch Blocks

try {
    // statements
} catch (FirstType e) {
    // handle FirstType
} catch (SecondType e) {
    // handle SecondType
} catch (ThirdType e) {
    // handle ThirdType
}

Example — dividing two values supplied as text:

public class Ratio {
    static void ratio(String[] parts) {
        try {
            int a = Integer.parseInt(parts[0]);
            int b = Integer.parseInt(parts[1]);
            System.out.println(a + " / " + b + " = " + (a / b));
        } catch (ArithmeticException e) {
            System.out.println("Cannot divide by zero");
        } catch (NumberFormatException e) {
            System.out.println("Not a number: " + e.getMessage());
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("Two values are required");
        }
    }

    public static void main(String[] args) {
        ratio(new String[] {"10", "2"});
        ratio(new String[] {"10", "0"});
        ratio(new String[] {"10", "x"});
        ratio(new String[] {"10"});
    }
}

Output:

10 / 2 = 5
Cannot divide by zero
Not a number: For input string: "x"
Two values are required

How Java chooses a handler:

  1. When an exception is thrown in the try block, Java checks the catch clauses from top to bottom.
  2. The first clause whose type matches (the same class or a superclass of the thrown exception) runs.
  3. Only one catch block ever runs for a given exception. The remaining clauses are skipped, and execution continues after the whole statement.
  4. If no clause matches, the exception propagates to the caller.

In the third call, parseInt("x") throws NumberFormatException. Java tests ArithmeticException first — no match — then NumberFormatException — match — and runs that block. In the fourth call, parts[1] doesn't exist, so the third handler runs.

Each handler declares its own variable. Using the name e in every block is allowed because each catch parameter is scoped to its own block.

Order Matters: Specific Before General

Because Java picks the first matching clause, a handler for a parent type placed above a handler for its subclass would always win, and the subclass handler could never run. The compiler treats this as an error:

public class Order {
    public static void main(String[] args) {
        try {
            Integer.parseInt("x");
        } catch (Exception e) {
            System.out.println("general");
        } catch (NumberFormatException e) {
            System.out.println("number");
        }
    }
}

Compiler output:

Order.java:7: error: exception NumberFormatException has already been caught
        } catch (NumberFormatException e) {
          ^
1 error

NumberFormatException is a subclass of Exception, so the first clause already covers it. The fix is to order handlers from most specific to most general:

try {
    Integer.parseInt("x");
} catch (NumberFormatException e) {
    System.out.println("number");      // specific case first
} catch (Exception e) {
    System.out.println("general");     // everything else
}

The order only matters between types that are related by inheritance. ArithmeticException and NumberFormatException are unrelated siblings (both extend RuntimeException through different branches), so in the Ratio example they could be listed in any order.

A useful way to remember the rule: picture the class hierarchy as a tree. Handlers must go from the leaves toward the root.

Multi-Catch: One Handler for Several Types

When different exceptions need exactly the same handling, writing the same block several times is repetitive. Since Java 7, one catch clause can list several types separated by |:

try {
    // statements
} catch (TypeA | TypeB | TypeC e) {
    // runs for any of the three
}
try {
    int a = Integer.parseInt(parts[0]);
    int b = Integer.parseInt(parts[1]);
    System.out.println(a / b);
} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
    System.out.println("Expected two whole numbers, e.g. \"10 2\"");
} catch (ArithmeticException e) {
    System.out.println("Cannot divide by zero");
}

Here missing input and non-numeric input both mean "the user typed the wrong thing", so they share one message, while division by zero gets its own.

Multi-catch has three rules, each with a reason.

import java.io.*;

public class Related {
    public static void main(String[] args) {
        try {
            new FileReader("a.txt").close();
        } catch (FileNotFoundException | IOException e) {
            System.out.println(e);
        }
    }
}

Compiler output:

Related.java:6: error: Alternatives in a multi-catch statement cannot be related by subclassing
        } catch (FileNotFoundException | IOException e) {
                                         ^
  Alternative FileNotFoundException is a subclass of alternative IOException
1 error

IOException already covers FileNotFoundException, so listing both is redundant. Just write catch (IOException e).

Rule 2: The parameter is implicitly final

public class Assign {
    public static void main(String[] args) {
        try {
            Integer.parseInt(args[0]);
        } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
            e = new NumberFormatException("replaced");
        }
    }
}

Compiler output:

Assign.java:6: error: multi-catch parameter e may not be assigned
            e = new NumberFormatException("replaced");
            ^
1 error

The variable could hold either type, so allowing reassignment would make its type unclear. (A single-type catch parameter can be reassigned, although doing so is rarely a good idea.)

Rule 3: The variable's type is the closest common parent

Inside catch (NumberFormatException | ArrayIndexOutOfBoundsException e), the compiler treats e as their nearest common supertype, RuntimeException. You can call any method available on that common type (getMessage(), getClass()), but not methods that only one of the alternatives has. The next example uses this to its advantage.

Choosing Between Separate Blocks and Multi-Catch

SituationUse
Each exception needs a different responseSeparate catch blocks
Several unrelated exceptions need identical handlingOne multi-catch clause listing them all
A parent type and one of its subclasses need different handlingSeparate blocks, subclass first
A parent type would cover everything you listA single catch of the parent type

Practical Example: Loading Application Settings

An application reads port and maxUsers from a .properties file at startup. Many different things can go wrong, and each deserves a precise message so whoever deploys the application can fix it quickly. If anything fails, the application falls back to safe defaults.

import java.io.IOException;
import java.nio.file.*;
import java.util.*;

public class Settings {
    record Config(int port, int maxUsers) { }

    static final Config DEFAULTS = new Config(8080, 100);

    static int intSetting(Properties p, String key) {
        String value = p.getProperty(key);
        if (value == null) {
            throw new IllegalArgumentException("missing setting '" + key + "'");
        }
        return Integer.parseInt(value.trim());
    }

    static Config load(Path file) {
        try {
            Properties p = new Properties();
            try (var reader = Files.newBufferedReader(file)) {
                p.load(reader);
            }
            int port = intSetting(p, "port");
            int maxUsers = intSetting(p, "maxUsers");
            if (port < 1 || port > 65535) {
                throw new IllegalArgumentException("port out of range: " + port);
            }
            return new Config(port, maxUsers);
        } catch (NoSuchFileException | AccessDeniedException e) {
            System.out.println("  file not available: " + Path.of(e.getFile()).getFileName()
                    + " (" + e.getClass().getSimpleName() + ")");
        } catch (IOException e) {
            System.out.println("  could not read file: " + e);
        } catch (NumberFormatException e) {
            System.out.println("  not a whole number: " + e.getMessage());
        } catch (IllegalArgumentException e) {
            System.out.println("  invalid configuration: " + e.getMessage());
        }
        System.out.println("  using defaults");
        return DEFAULTS;
    }

    public static void main(String[] args) throws IOException {
        Path dir = Files.createTempDirectory("cfg");
        Map<String, String> files = new LinkedHashMap<>();
        files.put("good.properties", "port=9090\nmaxUsers=500\n");
        files.put("text.properties", "port=ninety\nmaxUsers=500\n");
        files.put("partial.properties", "port=9090\n");
        files.put("range.properties", "port=70000\nmaxUsers=500\n");
        for (var entry : files.entrySet()) {
            Files.writeString(dir.resolve(entry.getKey()), entry.getValue());
        }
        List<String> names = new ArrayList<>(files.keySet());
        names.add("absent.properties");
        for (String name : names) {
            System.out.println(name);
            System.out.println("  -> " + load(dir.resolve(name)));
        }
    }
}

Output:

good.properties
  -> Config[port=9090, maxUsers=500]
text.properties
  not a whole number: For input string: "ninety"
  using defaults
  -> Config[port=8080, maxUsers=100]
partial.properties
  invalid configuration: missing setting 'maxUsers'
  using defaults
  -> Config[port=8080, maxUsers=100]
range.properties
  invalid configuration: port out of range: 70000
  using defaults
  -> Config[port=8080, maxUsers=100]
absent.properties
  file not available: absent.properties (NoSuchFileException)
  using defaults
  -> Config[port=8080, maxUsers=100]

main creates a temporary folder with four test files and then tries to load each, plus one file that doesn't exist. The interesting part is the catch chain in load:

  1. NoSuchFileException | AccessDeniedException — "the file can't be opened" is handled the same way whether it's missing or not readable, so a multi-catch fits. Both classes extend java.nio.file.FileSystemException, which is the common type of e. That's why e.getFile() compiles: getFile() is defined on FileSystemException, so it is available for either alternative (Rule 3).
  2. IOException — any other I/O failure, such as a disk error while reading. It must come after the first handler, because both NoSuchFileException and AccessDeniedException are IOException subclasses. Swapping them would cause the "has already been caught" compile error.
  3. NumberFormatException before IllegalArgumentException — this ordering is easy to miss. NumberFormatException extends IllegalArgumentException. Listed this way, "ninety" gets the specific "not a whole number" message, while the missing key and the out-of-range port — which the code reports by throwing IllegalArgumentException itself — get the "invalid configuration" message. If IllegalArgumentException came first, the code wouldn't compile.
  4. The successful path returns from inside the try. For every failure, a handler prints a message, execution continues after the try statement, prints "using defaults" and returns DEFAULTS. The fallback is written once instead of in every handler.

Two features from later lessons appear here briefly: throw new IllegalArgumentException(...) creates and throws an exception (the Throw Keyword lesson), and the inner try (var reader = ...) closes the file automatically (the Try With Resources lesson).

Note the design choice in intSetting: a missing key is detected with an explicit null check and reported with a clear message. The alternative — letting value.trim() throw NullPointerException and catching that — would mix real bugs with configuration problems and would produce a much less useful message.

Exceptions Thrown Inside a catch Block

The catch blocks of one statement only protect the try block. An exception thrown inside a catch block is not handled by its sibling clauses:

public class Nested {
    public static void main(String[] args) {
        try {
            Integer.parseInt("seven");
        } catch (NumberFormatException e) {
            System.out.println("Handling: " + e.getMessage());
            String fallback = null;
            System.out.println(fallback.length());
        } catch (NullPointerException e) {
            System.out.println("Caught NPE");
        }
        System.out.println("done");
    }
}

Output:

Handling: For input string: "seven"
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "fallback" is null
	at Nested.main(Nested.java:8)

The NullPointerException happens while the first handler is running. The second catch (NullPointerException e) clause does not catch it, because it only applies to exceptions from the try block. The new exception propagates out of the whole statement, so "Caught NPE" and "done" are never printed.

If handler code itself can fail, it needs its own try...catch inside the catch block, or — better — the handler should be simple enough not to fail.

The helpful NPE message names the local variable fallback only when the class was compiled with debug information for local variables (javac -g). Without it, the message refers to the variable by slot number, such as "<local2>". Build tools like Maven and Gradle include this information by default.

Catching a General Type as the Last Handler

A final catch (Exception e) after specific handlers is common at application boundaries — the top of a request handler, a scheduled job or a message consumer — where one failed unit of work must not bring down the whole process:

for (Job job : jobs) {
    try {
        job.run();
    } catch (TimeoutException e) {
        retryLater(job);
    } catch (Exception e) {
        log.error("Job " + job.id() + " failed", e);  // log with stack trace, continue with the next job
    }
}

Deep inside business logic, however, a broad final handler is usually a mistake: it catches bugs such as NullPointerException together with the expected failures and makes them harder to find. Catch broadly only where your response — log and move on — is genuinely right for any failure.

0 Comments

Reviewed before they appear

No comments yet.

Java
Java Introduction Java Features Java Syntax Java Comments Java Statements Java Output Java Variables Java Data Types Java Type Casting Java Operators Java Strings Java Numbers Java Math Java Booleans Java User Input Java Conditions Java If Else Java Switch Java While Loop Java For Loop Java Break Java Continue Java Arrays Java Methods Java Parameters Java Return Values Java Scope Java Recursion Install Java Install JDK Java IDEs Java JDK Java JRE Java JVM Java Compiler Java File Compile Java Run Java JShell Basics Java Packages Java OOP Java Classes Java Objects Class Methods Constructors Constructor Overload this Keyword static Keyword final Keyword Encapsulation Inheritance Polymorphism Abstraction Access Modifiers super Keyword Method Overload Method Override Instanceof Operator Nested Classes Inner Classes Anonymous Classes Enums Records Array Basics Array Length Loop Arrays Multidimensional Arrays Array Copy Array Sorting Array Search ArrayList LinkedList HashSet TreeSet HashMap TreeMap Queue Interface Deque Interface Stack Class Iterator List Interface Set Interface Map Interface Collections Class Comparable Comparator Generics Basics Generic Classes Generic Methods Wildcards String Basics String Length String Methods String Concatenation String Compare String Search String Replace String Split String Format StringBuilder StringBuffer Text Blocks Regular Expressions Number Types Integer Class Float Class Double Class BigInteger BigDecimal Math Class Random Numbers Number Format Date Basics LocalDate LocalTime LocalDateTime ZonedDateTime Date Formatting Date Parsing Period Class Duration Class Java Instant Class Java Exception Basics Java try...catch Java Multiple Catch Blocks Java finally Block Java throw Keyword Java throws Keyword Java Checked Exceptions Java Unchecked Exceptions Java Custom Exceptions Java try-with-resources Java Exception Chaining
Ask about this post
AI Ask about this post

Ask questions about Java Multiple Catch Blocks and get answers drawn from it.

Signed-in readers only.