Table of Contents
- Introduction to Exception Handling
- Multiple Catch Blocks Syntax
- Exception Hierarchy and Ordering
- Practical Examples
- Multi-catch Block (Java 7+)
- Best Practices
- Common Pitfalls
- Real-World Scenarios
Introduction to Exception Handling
Exception handling is a crucial mechanism in Java that allows developers to handle runtime errors gracefully, maintaining normal application flow. Multiple catch blocks enable you to handle different types of exceptions in different ways, providing precise error handling for various exceptional conditions.
Multiple Catch Blocks Syntax
Basic Structure
try {
// Code that may throw exceptions
} catch (ExceptionType1 e1) {
// Handle ExceptionType1
} catch (ExceptionType2 e2) {
// Handle ExceptionType2
} catch (ExceptionType3 e3) {
// Handle ExceptionType3
} finally {
// Optional: Code that always executes
}
Exception Hierarchy and Ordering
Understanding Exception Hierarchy
// Exception Hierarchy (simplified) java.lang.Object ↳ java.lang.Throwable ↳ java.lang.Exception ↳ java.lang.RuntimeException (Unchecked) ↳ Other Checked Exceptions ↳ java.lang.Error
Correct Ordering Principle
Specific exceptions must come before general ones. The Java compiler enforces this rule to prevent unreachable code.
// CORRECT: Specific to general
try {
// risky code
} catch (FileNotFoundException e) {
// Handle file not found
} catch (IOException e) {
// Handle other IO exceptions
} catch (Exception e) {
// Handle any other exceptions
}
// COMPILER ERROR: Unreachable code
try {
// risky code
} catch (Exception e) {
// This would catch ALL exceptions
} catch (IOException e) { // ERROR: Unreachable!
// This block can never be executed
}
Practical Examples
Example 1: File Processing with Multiple Exceptions
import java.io.*;
import java.nio.file.*;
import java.util.*;
public class FileProcessor {
public void processFile(String filePath) {
try {
// Read all lines from file
List<String> lines = Files.readAllLines(Paths.get(filePath));
// Process each line
for (int i = 0; i < lines.size(); i++) {
String line = lines.get(i);
int number = Integer.parseInt(line.trim());
System.out.println("Processed number: " + (number * 2));
}
} catch (NoSuchFileException e) {
System.err.println("Error: File '" + filePath + "' does not exist.");
} catch (AccessDeniedException e) {
System.err.println("Error: Access denied to file '" + filePath + "'.");
} catch (NumberFormatException e) {
System.err.println("Error: Invalid number format in file.");
} catch (IOException e) {
System.err.println("Error: General IO error - " + e.getMessage());
} catch (Exception e) {
System.err.println("Error: Unexpected error - " + e.getMessage());
}
}
}
Example 2: Database Operations
import java.sql.*;
import java.util.*;
public class DatabaseManager {
private Connection connection;
public void executeUserQuery(int userId, String query) {
try {
// Establish database connection
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "user", "pass");
// Execute query
PreparedStatement stmt = connection.prepareStatement(query);
stmt.setInt(1, userId);
ResultSet rs = stmt.executeQuery();
// Process results
while (rs.next()) {
System.out.println("User: " + rs.getString("username"));
}
} catch (SQLSyntaxErrorException e) {
System.err.println("SQL Syntax Error: " + e.getMessage());
logError("SQL Syntax", e);
} catch (SQLDataException e) {
System.err.println("Data Error: Invalid data format");
logError("Data Format", e);
} catch (SQLTimeoutException e) {
System.err.println("Database timeout occurred");
retryOperation();
} catch (SQLException e) {
System.err.println("General SQL Error: " + e.getErrorCode() + " - " + e.getMessage());
logError("General SQL", e);
} catch (Exception e) {
System.err.println("Unexpected error: " + e.getMessage());
logError("Unexpected", e);
} finally {
// Always close connection
closeConnection();
}
}
private void logError(String type, Exception e) {
// Implementation for error logging
System.out.println("Logged " + type + " error: " + new Date());
}
private void retryOperation() {
// Implementation for retry logic
System.out.println("Retrying operation...");
}
private void closeConnection() {
try {
if (connection != null && !connection.isClosed()) {
connection.close();
}
} catch (SQLException e) {
System.err.println("Error closing connection: " + e.getMessage());
}
}
}
Example 3: Network Operations
import java.net.*;
import java.io.*;
public class NetworkClient {
public void connectToServer(String host, int port) {
Socket socket = null;
try {
// Create socket connection
socket = new Socket(host, port);
System.out.println("Connected to " + host + ":" + port);
// Set timeout
socket.setSoTimeout(5000);
// Send data
OutputStream output = socket.getOutputStream();
PrintWriter writer = new PrintWriter(output, true);
writer.println("Hello Server!");
// Receive response
InputStream input = socket.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(input));
String response = reader.readLine();
System.out.println("Server response: " + response);
} catch (UnknownHostException e) {
System.err.println("Error: Unknown host '" + host + "'");
} catch (ConnectException e) {
System.err.println("Error: Connection refused to " + host + ":" + port);
} catch (SocketTimeoutException e) {
System.err.println("Error: Connection timeout after 5 seconds");
} catch (SSLHandshakeException e) {
System.err.println("Error: SSL handshake failed");
} catch (IOException e) {
System.err.println("Error: Network IO error - " + e.getMessage());
} finally {
// Always close socket
if (socket != null && !socket.isClosed()) {
try {
socket.close();
System.out.println("Connection closed");
} catch (IOException e) {
System.err.println("Error closing socket: " + e.getMessage());
}
}
}
}
}
Multi-catch Block (Java 7+)
Introduction to Multi-catch
Java 7 introduced the multi-catch block, allowing you to catch multiple exception types in a single catch block.
Multi-catch Syntax
try {
// Code that may throw multiple exceptions
} catch (ExceptionType1 | ExceptionType2 | ExceptionType3 e) {
// Handle all specified exception types
}
Multi-catch Examples
// Example 1: File operations with multi-catch
public void readConfigurationFile(String configPath) {
try {
Properties props = new Properties();
props.load(new FileInputStream(configPath));
String timeoutStr = props.getProperty("timeout");
int timeout = Integer.parseInt(timeoutStr);
} catch (FileNotFoundException | SecurityException e) {
System.err.println("File access error: " + e.getMessage());
} catch (NumberFormatException | IllegalArgumentException e) {
System.err.println("Invalid configuration value: " + e.getMessage());
} catch (IOException e) {
System.err.println("IO error reading configuration: " + e.getMessage());
}
}
// Example 2: Web service calls
public void callWebService(String url) {
try {
URL serviceUrl = new URL(url);
HttpURLConnection connection = (HttpURLConnection) serviceUrl.openConnection();
// Set request properties
connection.setRequestMethod("GET");
connection.setConnectTimeout(3000);
int responseCode = connection.getResponseCode();
System.out.println("Response Code: " + responseCode);
} catch (MalformedURLException | ProtocolException e) {
System.err.println("Invalid URL or protocol: " + e.getMessage());
} catch (SocketTimeoutException | ConnectException e) {
System.err.println("Connection timeout or refused: " + e.getMessage());
retryWebServiceCall(url);
} catch (IOException e) {
System.err.println("Network error: " + e.getMessage());
}
}
Important Multi-catch Rules
// VALID: Exceptions in same hierarchy level
catch (IOException | SQLException e) { }
// VALID: Exceptions with common ancestor
catch (FileNotFoundException | EOFException e) { } // Both extend IOException
// INVALID: Exceptions in inheritance relationship
catch (FileNotFoundException | IOException e) { } // COMPILER ERROR!
// VALID: With final or effectively final variable
catch (IOException | SQLException e) {
// 'e' is effectively final
logError(e); // Can use e
// e = new IOException(); // ERROR - e is final
}
Best Practices
1. Order Specific to General
// GOOD
try {
// operations
} catch (SpecificException e) {
// handle specific case
} catch (GeneralException e) {
// handle general case
}
// BAD - compiler error
try {
// operations
} catch (GeneralException e) {
// handle general case
} catch (SpecificException e) { // UNREACHABLE!
// handle specific case
}
2. Don't Catch Throwable
// AVOID - catches everything including Errors
try {
// code
} catch (Throwable t) { // Too broad!
// handling
}
// PREFER - catch specific exceptions
try {
// code
} catch (SpecificException e) {
// specific handling
} catch (Exception e) {
// general exception handling
}
3. Use Multi-catch for Related Exceptions
// When handling is identical for multiple exceptions
try {
// database operations
} catch (SQLSyntaxErrorException | SQLDataException e) {
// Both are data-related errors
logger.log("Data error: " + e.getMessage());
rollbackTransaction();
}
4. Don't Ignore Exceptions
// BAD - silent failure
try {
riskyOperation();
} catch (Exception e) {
// Empty catch block - exception ignored!
}
// GOOD - at least log it
try {
riskyOperation();
} catch (Exception e) {
logger.error("Operation failed", e);
}
// BETTER - handle appropriately
try {
riskyOperation();
} catch (SpecificException e) {
recoverFromError();
} catch (AnotherException e) {
notifyUser();
}
5. Use Finally for Cleanup
public void processResource() {
Connection conn = null;
PreparedStatement stmt = null;
try {
conn = getConnection();
stmt = conn.prepareStatement("SELECT * FROM users");
// ... process results
} catch (SQLException e) {
handleDatabaseError(e);
} finally {
// Always cleanup resources
try {
if (stmt != null) stmt.close();
if (conn != null) conn.close();
} catch (SQLException e) {
logger.warn("Error closing resources", e);
}
}
}
Common Pitfalls
1. Overly Broad Catch Blocks
// PROBLEMATIC: Hides specific errors
try {
readFile();
parseData();
saveToDatabase();
} catch (Exception e) { // Too broad!
System.out.println("Something went wrong");
}
// BETTER: Specific handling
try {
readFile();
} catch (FileNotFoundException e) {
createDefaultFile();
} catch (IOException e) {
logger.error("File read error", e);
}
try {
parseData();
} catch (ParseException e) {
logger.error("Data parsing error", e);
}
try {
saveToDatabase();
} catch (SQLException e) {
logger.error("Database error", e);
}
2. Resource Leakage
// RISKY: Potential resource leak
try {
FileInputStream fis = new FileInputStream("file.txt");
// process file
fis.close(); // Might not execute if exception occurs
} catch (IOException e) {
// handle
}
// BETTER: Use try-with-resources (Java 7+)
try (FileInputStream fis = new FileInputStream("file.txt")) {
// process file
// fis automatically closed
} catch (IOException e) {
// handle
}
3. Exception Masking
// PROBLEM: Original exception masked
try {
riskyOperation();
} catch (Exception e) {
throw new RuntimeException("Operation failed"); // Original exception lost!
}
// BETTER: Preserve original exception
try {
riskyOperation();
} catch (Exception e) {
throw new RuntimeException("Operation failed", e); // Include cause
}
Real-World Scenarios
Scenario 1: Web Application Controller
@RestController
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/users")
public ResponseEntity<?> createUser(@RequestBody User user) {
try {
User createdUser = userService.createUser(user);
return ResponseEntity.ok(createdUser);
} catch (DuplicateUserException e) {
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(new ErrorResponse("User already exists"));
} catch (InvalidDataException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse("Invalid user data: " + e.getMessage()));
} catch (ServiceUnavailableException e) {
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(new ErrorResponse("Service temporarily unavailable"));
} catch (Exception e) {
logger.error("Unexpected error creating user", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("Internal server error"));
}
}
}
Scenario 2: Batch Data Processing
public class DataBatchProcessor {
public void processBatch(List<String> dataLines) {
int successCount = 0;
int errorCount = 0;
for (int i = 0; i < dataLines.size(); i++) {
try {
processSingleRecord(dataLines.get(i), i + 1);
successCount++;
} catch (DataFormatException e) {
errorCount++;
logger.warn("Invalid data format at line " + (i + 1) + ": " + e.getMessage());
continue; // Continue with next record
} catch (DatabaseConstraintException e) {
errorCount++;
logger.error("Database constraint violation at line " + (i + 1), e);
continue;
} catch (ExternalServiceException e) {
errorCount++;
logger.error("External service failure at line " + (i + 1), e);
// Maybe break or continue based on business logic
if (e.isCritical()) {
throw new BatchProcessingException("Critical failure", e);
}
continue;
} catch (Exception e) {
errorCount++;
logger.error("Unexpected error processing line " + (i + 1), e);
continue;
}
}
logger.info("Batch processing completed: " + successCount +
" successful, " + errorCount + " errors");
}
private void processSingleRecord(String data, int lineNumber)
throws DataFormatException, DatabaseConstraintException, ExternalServiceException {
// Implementation details
}
}
Scenario 3: Financial Transaction System
public class TransactionProcessor {
public TransactionResult processTransaction(Transaction transaction) {
try {
validateTransaction(transaction);
checkFraud(transaction);
processPayment(transaction);
updateAccounts(transaction);
return TransactionResult.success(transaction.getId());
} catch (InvalidAmountException e) {
logger.warn("Invalid amount in transaction: " + transaction.getId());
return TransactionResult.failure("INVALID_AMOUNT", e.getMessage());
} catch (InsufficientFundsException e) {
logger.warn("Insufficient funds for transaction: " + transaction.getId());
return TransactionResult.failure("INSUFFICIENT_FUNDS", e.getMessage());
} catch (FraudDetectionException e) {
logger.error("Fraud detected in transaction: " + transaction.getId());
blockAccount(transaction.getFromAccount());
return TransactionResult.failure("FRAUD_DETECTED", "Transaction blocked");
} catch (NetworkTimeoutException e) {
logger.error("Network timeout processing transaction: " + transaction.getId());
// Mark for retry
retryService.scheduleRetry(transaction);
return TransactionResult.failure("NETWORK_ERROR", "Will retry");
} catch (DatabaseException e) {
logger.error("Database error processing transaction: " + transaction.getId(), e);
// Critical error - need manual intervention
alertAdministrator(transaction, e);
return TransactionResult.failure("SYSTEM_ERROR", "Contact support");
} catch (Exception e) {
logger.error("Unexpected error processing transaction: " + transaction.getId(), e);
return TransactionResult.failure("UNKNOWN_ERROR", "System error");
}
}
}
Summary
Multiple catch blocks in Java provide a powerful mechanism for handling different types of exceptions appropriately. Key takeaways:
- Order matters - specific exceptions before general ones
- Use multi-catch for related exceptions with identical handling
- Avoid overly broad catch blocks that hide problems
- Always cleanup resources using finally blocks or try-with-resources
- Log appropriately for debugging and monitoring
- Consider the business context when deciding how to handle exceptions
Proper exception handling with multiple catch blocks makes your applications more robust, maintainable, and user-friendly.