☕ Java
Multiple catch
A single try block can be followed by multiple catch blocks, each handling a different exception type. Multiple catch blocks allow code to respond differently to different kinds of failures — recovering from an expected missing file, but reporting and rethrowing an unexpected database error. Java 7 introduced multi-catch syntax, allowing a single catch block to handle multiple unrelated exception types, eliminating the duplicate code that arises when different exceptions require the same handling. This entry covers the ordering rules, multi-catch syntax, the type of the caught variable in multi-catch, and the design patterns for structuring multiple exception handlers.
Multiple Catch Blocks — Ordering and Matching
When a try block is followed by multiple catch blocks, the JVM evaluates them top to bottom and executes the first one that matches the thrown exception. This first-match semantics has an important consequence for ordering: more specific exception types must precede more general ones. If a general type appears first, it matches all subtypes, making the specific catch blocks below it unreachable — the compiler detects this and reports a compile error.
The ordering rule gives you control over differentiated handling. A try block that might throw either FileNotFoundException (expected — file may legitimately not exist) or IOException (unexpected — disk error, permission problem) can handle them separately: the specific FileNotFoundException catch can recover gracefully (use a default), while the general IOException catch can log and rethrow. Without this ordering, both would collapse into the IOException handler with no way to distinguish them.
Each catch block is entirely independent. Variables declared in the try block and in scope at the throw point are accessible in all catch blocks (if definitely assigned before the throw). The exception variable declared in each catch clause is scoped to that specific catch block and invisible to the others. After any one catch block executes, the remaining catch blocks for that try block are skipped — only one catch block runs per thrown exception.
Java
// ── Multiple catch blocks — most specific first ──────────────────────
public String loadUserData(long userId) {
try {
return database.query(
"SELECT data FROM users WHERE id = ?", userId);
} catch (UserNotFoundException e) {
// Specific: known condition — user doesn't exist
return DEFAULT_USER_DATA;
} catch (DatabaseTimeoutException e) {
// Specific: known condition — try cache instead
return cache.getUserData(userId);
} catch (SQLException e) {
// General: unexpected database problem — escalate
throw new ServiceException(
"Failed to load user " + userId, e);
}
// Only ONE catch block runs per exception
}
// ── Compiler rejects unreachable blocks ───────────────────────────────
try {
riskyOp();
// } catch (Exception e) { // COMPILE ERROR if this comes first:
// ... // the IOException block below is unreachable
// } catch (IOException e) { // Exception IS-A parent of IOException
// ...
// }
} catch (IOException e) { // specific first — correct
...
} catch (Exception e) { // general last — reachable for non-IOException
...
}
// ── Independent exception variables ──────────────────────────────────
try {
process();
} catch (IOException e) {
System.out.println("IO: " + e.getMessage());
// 'e' is IOException here
} catch (SQLException e) {
System.out.println("SQL: " + e.getMessage());
// 'e' is SQLException here — different variable, different scope
// Previous IOException 'e' is not accessible here
}
// ── Only one catch block executes ────────────────────────────────────
try {
throw new FileNotFoundException("test.txt");
} catch (FileNotFoundException e) {
System.out.println("Caught FileNotFoundException"); // THIS runs
} catch (IOException e) {
System.out.println("Caught IOException"); // NOT reached — already caught above
} catch (Exception e) {
System.out.println("Caught Exception"); // NOT reached
}Multi-Catch — Handling Multiple Types in One Block
Java 7 introduced multi-catch syntax: a single catch block that handles multiple exception types separated by the pipe character (|). Multi-catch is purely a syntactic convenience — it eliminates duplicated catch block bodies when different exception types require identical handling. It has no effect on the exception propagation model or matching rules.
The type of the caught exception variable in a multi-catch block is the most specific common supertype of all listed types. If the types are unrelated (they share only Exception or Throwable as a common ancestor), the variable's type is their union type, which the compiler treats as effectively the union. Practically, this means only methods declared on the common supertype are accessible through the variable without casting.
Multi-catch has an important restriction: the listed types must not be in a supertype-subtype relationship with each other. Writing catch (IOException | FileNotFoundException e) is a compile error because FileNotFoundException is a subtype of IOException — the FileNotFoundException case is already covered by IOException. This restriction prevents redundancy and makes the developer's intent clear.
Java
// ── Without multi-catch — duplicated handling code ───────────────────
public void saveUser(User user) {
try {
userRepo.save(user);
emailService.sendConfirmation(user.getEmail());
} catch (DatabaseException e) {
log.error("Failed to save user {}", user.getId(), e);
auditLog.record("USER_SAVE_FAILED", user.getId());
throw new ServiceException("User save failed", e);
} catch (EmailException e) {
// SAME handling as DatabaseException — duplicated code
log.error("Failed to save user {}", user.getId(), e);
auditLog.record("USER_SAVE_FAILED", user.getId());
throw new ServiceException("User save failed", e);
}
}
// ── With multi-catch — DRY, single handler for both ───────────────────
public void saveUser_clean(User user) {
try {
userRepo.save(user);
emailService.sendConfirmation(user.getEmail());
} catch (DatabaseException | EmailException e) {
// One block handles both types identically
log.error("Failed to save user {}", user.getId(), e);
auditLog.record("USER_SAVE_FAILED", user.getId());
throw new ServiceException("User save failed", e);
}
}
// ── Type of variable in multi-catch ──────────────────────────────────
try {
riskyOp();
} catch (IOException | SQLException e) {
// e's type is effectively: IOException | SQLException
// Only methods from their common ancestor (Exception) are available:
e.getMessage(); // OK — declared on Throwable
e.getCause(); // OK — declared on Throwable
e.printStackTrace();// OK — declared on Throwable
// e.getErrorCode() // COMPILE ERROR — only on SQLException, not IOException
}
// ── Multi-catch variable is effectively final ─────────────────────────
try {
riskyOp();
} catch (IOException | SQLException e) {
// e = new RuntimeException(); // COMPILE ERROR — e is effectively final
// This prevents changing which exception was caught
throw e; // rethrow is allowed
}
// ── Compiler rejects subtype relationships ────────────────────────────
try {
riskyOp();
// } catch (IOException | FileNotFoundException e) { // COMPILE ERROR
// // FileNotFoundException IS-A IOException — redundant
// }
} catch (IOException e) { // correct — covers both
...
}Differentiated Handling Patterns
The power of multiple catch blocks lies in differentiated handling — each exception type gets the response it semantically requires. The practical patterns fall into three categories: recover differently for different failure types; escalate with different severity or metadata based on the type; or normalise multiple failure types into a single domain exception for the caller.
The recover-and-retry pattern handles transient failures (network timeouts, temporary unavailability) differently from permanent failures (not found, permission denied). A transient failure might trigger a retry with backoff; a permanent failure fails immediately. Multiple catch blocks allow this distinction without instanceof checks.
The structured logging pattern assigns different log levels to different exceptions: expected, recoverable failures (FileNotFoundException) at DEBUG or WARN; unexpected failures (NullPointerException, unexpected RuntimeException) at ERROR. This keeps operational logs clean during normal operation while ensuring genuine errors are visible.
Java
// ── Recover-differently pattern ──────────────────────────────────────
public Order fetchOrder(Long orderId) {
try {
return orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
} catch (OrderNotFoundException e) {
// Permanent — order does not exist, fail immediately
throw e;
} catch (DatabaseTimeoutException e) {
// Transient — try cache fallback
log.warn("DB timeout fetching order {}, trying cache", orderId);
return orderCache.get(orderId)
.orElseThrow(() -> new ServiceException(
"Order unavailable: " + orderId, e));
} catch (DataAccessException e) {
// Unexpected — escalate with context
throw new ServiceException(
"Unexpected error fetching order " + orderId, e);
}
}
// ── Structured log levels pattern ────────────────────────────────────
public void processPayment(PaymentRequest req) {
try {
paymentGateway.charge(req);
} catch (CardDeclinedException e) {
// Expected business condition — INFO or WARN level
log.warn("Card declined for order {} reason={}",
req.getOrderId(), e.getDeclineCode());
throw e;
} catch (PaymentGatewayTimeoutException e) {
// Transient infrastructure issue — WARN level
log.warn("Payment gateway timeout for order {}, attempt {}",
req.getOrderId(), req.getAttemptNumber(), e);
throw e;
} catch (Exception e) {
// Unexpected — ERROR level with full context
log.error("Unexpected payment failure for order {}",
req.getOrderId(), e);
throw new PaymentException(
"Unexpected payment failure", e);
}
}
// ── Normalise to domain exception ────────────────────────────────────
public UserDto getUserDto(Long userId) {
try {
User user = userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException(userId));
return UserDto.from(user);
} catch (UserNotFoundException e) {
throw e; // already a domain exception — pass through
} catch (DataAccessException | JdbcException e) {
// Multiple infrastructure exceptions → single domain exception
throw new UserServiceException(
"Failed to retrieve user " + userId, e);
}
}Related Topics in Exception Handling
Exception Basics
An exception is an event that disrupts the normal flow of a program during execution. Java's exception system is a structured mechanism for signalling, propagating, and handling error conditions — a significant improvement over the older approach of returning special error codes that callers could silently ignore. Understanding exceptions means understanding the class hierarchy that categorises them, the distinction between checked and unchecked exceptions, what the JVM does when an exception is thrown, how the call stack is unwound, and what information an exception object carries. This entry covers the full exception hierarchy, the checked vs unchecked distinction and the reasoning behind it, exception propagation through the call stack, and the information model of exception objects.
try-catch
The try-catch statement is Java's primary mechanism for handling exceptions. Code that might throw an exception is placed in the try block; one or more catch blocks follow, each specifying the exception type to handle and providing the handling logic. When an exception is thrown inside the try block, execution of the try block immediately stops and the JVM searches the catch blocks in order for the first one whose type matches the thrown exception. Understanding the control flow precisely — what runs, what stops, and in what order — is essential for writing correct exception handling code. This entry covers try-catch control flow, exception type matching, catching by supertype, variable scope, and the key principles for writing good catch blocks.
finally
The finally block contains code that must execute regardless of whether the try block completed normally, threw an exception that was caught, or threw an exception that was not caught. It is the mechanism for guaranteed cleanup — closing files, releasing locks, returning connections to a pool, rolling back transactions. The finally block runs in all scenarios except two: if the JVM exits (System.exit()) or if the thread is killed by an Error like StackOverflowError. Understanding when exactly finally runs, the interaction between finally and return statements, exception suppression when finally itself throws, and the modern alternative of try-with-resources is essential for writing correct resource management code in Java.
throw
The throw statement explicitly throws an exception object, immediately terminating normal execution and initiating exception propagation. throw is used to signal that a method cannot fulfil its contract because a precondition is violated, an invalid state has been detected, or an operation has failed. Understanding when to throw, what to throw, how to create informative exception objects, and how throwing interacts with the call stack is the foundation of using exceptions as a design tool rather than just an error mechanism. This entry covers the throw statement mechanics, throwing at the right abstraction level, creating informative exception messages, exception chaining, and the principles for deciding when to throw versus returning a special value.