Exception handling in Java provides a structured way to respond to conditions that interrupt normal program execution, such as file-access failures, invalid input, or database errors. It allows applications to recover when possible or report failures clearly, making code easier to maintain and troubleshoot.
- Use specific exception types and catch them where recovery or a meaningful response is possible.
- Retain the original cause for diagnosis and use try-with-resources to close files, streams, and database connections safely.
Why Is Proper Exception Handling Important?
Poor exception handling can hide failures, make debugging difficult, and leave resources open. A well-designed exception-handling strategy helps applications respond to errors without introducing unexpected behavior.
For example, if a file cannot be opened, an application can display a meaningful error message instead of terminating with an unexplained failure.
Practices for Handling Exceptions
Effective exception handling involves choosing suitable exception types, deciding where to handle them, and managing resources correctly. The following practices help keep error-handling code clear and consistent.
1. Catch Specific Exceptions
Catch exceptions that accurately describe the failure rather than using a general Exception handler for every situation.
For example, use IOException for file-related errors and SQLException for database operations.
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
class GFG {
public static void main(String[] args) {
try {
String data = Files.readString(Path.of("data.txt"));
System.out.println(data);
} catch (IOException e) {
System.err.println("Unable to read the file.");
}
}
}
Explanation: The program catches IOException because reading a file may fail due to an unavailable file, insufficient permissions, or another I/O problem. Catching a specific exception makes the intended error handling clear.
Note: Custom exceptions, such as InsufficientFundsException, are useful when an application needs to represent a meaningful domain-specific failure.
2. Handle Exceptions at the Appropriate Level
An exception should be handled where meaningful action can be taken. If a method cannot recover from a failure, it can propagate the exception to its caller using throws.
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
class GFG{
static String readConfig() throws IOException {
return Files.readString(Path.of("config.txt"));
}
public static void main(String[] args) {
try {
System.out.println(readConfig());
} catch (IOException e) {
System.err.println("Unable to load configuration.");
}
}
}
Explanation: The readConfig() method propagates the file-reading exception because it has no recovery strategy. The main() method handles the exception and displays a meaningful message.
3. Avoid Empty Catch Blocks
An empty catch block silently ignores exceptions, making failures difficult to identify.
Incorrect Approach:
try {
int result = 10 / 0;
} catch (ArithmeticException e) {
// Exception ignored
}
Correct Approach:
try {
int result = 10 / 0;
} catch (ArithmeticException e) {
System.err.println("Arithmetic operation failed: "
+ e.getMessage());
}
Explanation: The corrected example reports the arithmetic error rather than ignoring it. In real applications, exceptions should be handled, propagated, or translated according to the operation's requirements.
Avoid returning misleading success values when an operation fails.
4. Preserve the Original Exception Cause
When converting one exception into another, retain the original exception using exception chaining.
import java.io.IOException;
class GFG {
static void loadData() throws IOException {
try {
int number = Integer.parseInt("abc");
} catch (NumberFormatException e) {
throw new IOException("Invalid numeric data", e);
}
}
public static void main(String[] args) {
try {
loadData();
} catch (IOException e) {
System.err.println(e.getMessage());
System.err.println("Cause: " + e.getCause());
}
}
}
Explanation: The NumberFormatException is wrapped inside an IOException using its cause parameter. This preserves the original error details and helps identify the underlying failure.
5. Use Try-With-Resources for Automatic Cleanup
Resources such as file streams and database connections must be closed after use. Java provides try-with-resources to automatically close objects implementing AutoCloseable.
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
class GFG {
public static void main(String[] args) {
try (BufferedReader reader =
Files.newBufferedReader(Path.of("data.txt"))) {
System.out.println(reader.readLine());
} catch (IOException e) {
System.err.println("File operation failed: "
+ e.getMessage());
}
}
}
Explanation: The BufferedReader is closed automatically when execution leaves the try block, including when an exception occurs.
Try-with-resources also closes multiple resources in reverse declaration order. If both the main operation and cleanup fail, the cleanup exception is retained as a suppressed exception.
Use finally for cleanup operations that are not handled through try-with-resources, such as releasing explicitly acquired locks.
6. Choose Checked and Unchecked Exceptions Appropriately
Java exceptions are commonly classified as checked and unchecked exceptions.
Checked Exceptions | Unchecked Exceptions |
|---|---|
Must be caught or declared using throws. | Do not require explicit handling or declaration. |
Often represent failures that callers are expected to consider. | Often represent invalid arguments, incorrect state, or programming errors. |
Examples: IOException, SQLException | Examples: NullPointerException, IllegalArgumentException |
Verified by the compiler. | Not subject to compile-time catch-or-declare checking. |
When to Use
- Use checked exceptions when the API contract requires callers to explicitly acknowledge a possible failure.
- Use unchecked exceptions when a failure represents invalid usage or a violated precondition.
The choice depends on the API contract. Checked exceptions do not guarantee recovery, and unchecked exceptions are not necessarily fatal.
7. Log Exceptions with Meaningful Context
Logging helps identify why and where an exception occurred. Use a logging framework to record diagnostic information instead of relying only on System.out.println() or printStackTrace().
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.logging.Level;
import java.util.logging.Logger;
class GFG {
private static final Logger LOGGER =
Logger.getLogger(GFG.class.getName());
public static void main(String[] args) {
try {
Files.readString(Path.of("data.txt"));
} catch (IOException e) {
LOGGER.log(Level.SEVERE,
"Failed to read application data", e);
}
}
}
Explanation: The logger records the failure message and exception details, including the stack trace.
Avoid logging passwords, authentication tokens, and sensitive personal information. Also, avoid repeatedly logging the same exception at multiple application layers.
Example 1: Copying a File Safely
The following program copies data from one file to another using try-with-resources and handles file-related exceptions.
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.logging.Level;
import java.util.logging.Logger;
public class FileCopyExample {
private static final Logger LOGGER
= Logger.getLogger(FileCopyExample.class.getName());
public static void main(String[] args)
{
try (InputStream input
= Files.newInputStream(Path.of("input.txt"));
OutputStream output = Files.newOutputStream(
Path.of("output.txt"))) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = input.read(buffer)) != -1) {
output.write(buffer, 0, bytesRead);
}
System.out.println("File copied successfully.");
}
catch (IOException e) {
LOGGER.log(Level.SEVERE, "File copying failed",
e);
System.err.println(
"Unable to copy the file. Check paths and permissions.");
}
}
}
Output
File copied successfully.
Explanation:
- Files.newInputStream() opens the source file for reading.
- Files.newOutputStream() opens the destination file for writing.
- The program copies the contents using a byte buffer.
- Try-with-resources closes both streams automatically.
- If an I/O operation fails, the exception is logged and an appropriate error message is displayed.
Note: The program overwrites an existing destination file. An interrupted copy may leave an incomplete output file.
Example 2: Handling Database Operations Using JDBC
Database operations may fail because of connection problems, invalid SQL statements, or database constraints. JDBC reports these failures through SQLException.
The following example retrieves a user's name using a configured DataSource.
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Optional;
import javax.sql.DataSource;
public class UserRepository {
public static Optional<String> findUserName(
DataSource dataSource, int userId)
throws SQLException {
String sql = "SELECT name FROM users WHERE id = ?";
try (Connection connection = dataSource.getConnection();
PreparedStatement statement =
connection.prepareStatement(sql)) {
statement.setInt(1, userId);
try (ResultSet result = statement.executeQuery()) {
if (result.next()) {
return Optional.ofNullable(
result.getString("name"));
}
return Optional.empty();
}
}
}
}
Explanation:
- The DataSource provides a database connection.
- PreparedStatement binds the user ID safely to the SQL query.
- ResultSet stores the returned database rows.
- Optional.empty() represents a missing record rather than an error.
- Database failures propagate as SQLException, allowing the caller to handle them appropriately.
- Try-with-resources closes the result set, statement, and connection automatically.
Requirements: Configure a JDBC driver and DataSource, and ensure the database contains a users table with id and name columns