Prefer Specific Exception Types
When catching exceptions, avoid broad exception types like Exception or Throwable. Instead, target specific exception classes that accurately describe what might go wrong. This practice makes code easier to understand and debug.
import java.io.FileInputStream;
import java.io.FileNotFoundException;
public class ExceptionTypeDemo {
public static void main(String[] args) {
try {
FileInputStream stream = new FileInputStream("config.xml");
byte[] buffer = stream.readAllBytes();
} catch (FileNotFoundException error) {
System.err.println("Configuration file missing: " + error.getMessage());
}
}
}
This approach clearly communicates the expected failure mode rather than masking it behind a generic exception handler.
Leverage try-with-resources
Java 7 introduced try-with-resources for automatic resource cleanup. This construct guarantees that resources implementing AutoCloseable are properly closed, even when exceptions occur.
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class ResourceManagementDemo {
public static void main(String[] args) {
String filePath = "data/input.txt";
try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) {
String content;
while ((content = reader.readLine()) != null) {
System.out.println(content);
}
} catch (IOException error) {
System.err.println("Read operation failed: " + error.getMessage());
}
}
}
The BufferedReader closes automatically when the try block exits, eliminating the need for explicit finally blocks and reducing boilerplate code.
Design Exception Propagation Strategically
Exception handling strategy depends on where the error can be meaningfully addressed. Sometimes you should handle an exception locally; other times you should let it propagate to the caller.
public class ExceptionFlowDemo {
public static void main(String[] args) {
try {
processOrder(1001);
} catch (OrderProcessingException error) {
System.err.println("Order processing failed: " + error.getMessage());
}
}
public static void processOrder(int orderId) throws OrderProcessingException {
validateOrder(orderId);
// Additional processing logic
}
public static void validateOrder(int orderId) throws OrderProcessingException {
if (orderId <= 0) {
throw new OrderProcessingException("Invalid order identifier");
}
}
static class OrderProcessingException extends Exception {
public OrderProcessingException(String message) {
super(message);
}
}
}
This pattern allows exceptions to bubble up to an appropriate level in the call stack where meaningful recovery or logging can occur.
Log Exceptions Appropriately
Exception logging should provide sufficient context for debugging without overwhelming logs with redundant enformation. Use structured logging frameworks to capture exception details systematically.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ExceptionLoggingDemo {
private static final Logger logger = LoggerFactory.getLogger(ExceptionLoggingDemo.class);
public void executeTask(String taskName) {
try {
performTask(taskName);
} catch (TaskExecutionException error) {
logger.error("Task '{}' encountered an error", taskName, error);
}
}
private void performTask(String name) throws TaskExecutionException {
throw new TaskExecutionException("Execution interrupted for task: " + name);
}
static class TaskExecutionException extends Exception {
public TaskExecutionException(String message) {
super(message);
}
}
}
Logging frameworks provide flexibility in output formatting and log levels, making it easier to filter and analyze exceptions in production environments.