Exception Handling

Multi-catch & Rethrowing

15 min Lesson 6 of 14

Multi-catch & Rethrowing

By lesson five you know how to throw and declare exceptions. This lesson covers two closely related techniques that make your error-handling code cleaner and more expressive: catching several exception types in a single block, and deliberately rethrowing — or wrapping — an exception so that the right layer of your program sees the right kind of error.

The problem with repetitive catch blocks

Suppose you are parsing user input and writing to a file. Both operations can fail in different ways, but your recovery action is identical: log the problem and return a default value. The naive approach leads to duplication:

try { int value = Integer.parseInt(input); Files.writeString(path, String.valueOf(value)); } catch (NumberFormatException e) { logger.error("Bad input", e); return DEFAULT; } catch (IOException e) { logger.error("Bad input", e); // identical body — code smell! return DEFAULT; }

Java 7 introduced multi-catch to eliminate this repetition.

Multi-catch syntax

Separate the exception types with a pipe character (|) inside a single catch clause. The parameter is implicitly final.

try { int value = Integer.parseInt(input); Files.writeString(path, String.valueOf(value)); } catch (NumberFormatException | IOException e) { logger.error("Operation failed", e); return DEFAULT; }

The two types are unrelated in the hierarchy, yet they share one handler. Java infers the catch parameter as the most specific common supertype — here Exception — which is why the parameter is implicitly final: you cannot reassign a reference whose exact runtime type the compiler cannot predict.

Order does not matter in multi-catch — unlike sequential catch blocks, the types in a pipe list are not evaluated top-to-bottom. They are all equally considered. You cannot list a supertype alongside one of its subtypes (e.g., Exception | IOException) because that would make the subtype unreachable; the compiler rejects it with a compile-time error.

When multi-catch is the right tool

  • The recovery logic is genuinely identical for all listed types.
  • The types are unrelated (no parent-child relationship between them).
  • You still want to preserve the original exception in a log or chain.

If the handling differs — say you want to retry on IOException but not on NumberFormatException — keep separate blocks.

Rethrowing the same exception

Sometimes you catch an exception only to add a log entry or release a resource, then let it propagate up unchanged. You do that with a plain throw e;:

public void processFile(Path path) throws IOException { try { String content = Files.readString(path); parse(content); } catch (IOException e) { logger.error("Failed to read {}", path, e); throw e; // rethrow the original, still typed as IOException } }

Since Java 7, the compiler is smart enough to know that only IOException can be thrown from the try block, so even though the catch parameter is typed as Exception, rethrowing it is allowed in a method declared throws IOException. This is called precise rethrow.

Exception wrapping (chaining)

When a low-level exception leaks out of a high-level method, the caller is forced to know about an implementation detail it should not care about. The solution is to wrap the original exception inside a new one that fits the abstraction, while preserving the cause for debugging.

public class UserRepository { public User findById(int id) { try { return database.query("SELECT * FROM users WHERE id = " + id); } catch (SQLException e) { // The caller should not need to know about SQL. // Wrap it in a domain-level exception and preserve the cause. throw new RepositoryException("Could not load user " + id, e); } } }

The constructor signature used above is the standard one for wrappable exceptions:

public class RepositoryException extends RuntimeException { public RepositoryException(String message, Throwable cause) { super(message, cause); } }

Now the stack trace printed by e.printStackTrace() shows both the high-level RepositoryException and the original SQLException under the heading "Caused by:", giving you everything you need for debugging without exposing the SQL layer to callers.

Always pass the original exception as the cause when wrapping. Writing throw new RepositoryException("Could not load user") — without the e — silently discards the root cause and makes bugs much harder to diagnose in production.

Rethrowing inside a multi-catch

You can combine both techniques. Here a utility method catches two unrelated exceptions, logs them with a single handler, then rethrows each wrapped as a domain exception:

public Config loadConfig(String fileName) { try { Path p = Paths.get(fileName); String json = Files.readString(p); return parseJson(json); } catch (IOException | JsonParseException e) { // single log statement covers both error types logger.error("Config load failed for {}", fileName, e); // wrap and rethrow — callers only see ConfigException throw new ConfigException("Unable to load config: " + fileName, e); } }
Do not catch-and-rethrow just to add noise. If you are not logging, not wrapping, and not releasing a resource — just writing catch (Exception e) { throw e; } — delete the block entirely. Pointless catch-rethrow clutters code and can even change the checked/unchecked contract in ways that surprise callers.

Summary

  • Multi-catch (catch (A | B e)) eliminates duplicate handlers when the recovery is identical; types must be unrelated in the hierarchy.
  • Rethrowing lets a layer log or release resources and then pass the exception up unchanged.
  • Exception wrapping translates a low-level exception into one that fits the current abstraction layer, using the cause constructor argument to preserve the original for debugging.
  • Always include the original exception as the cause when wrapping — never discard it silently.