Exception Handling Best Practices in Java
The rules that separate exception handling which helps from exception handling which hides bugs.
- Never swallow an exception
- Catch what you can handle
- Fail fast at the boundary
- Do not use exceptions for control flow
- Write messages that help
- Always preserve the cause
- Log or rethrow, not both
- Use try with resources
- Do not catch what you cannot fix
- Clean up in the right place
- Keep the try block small
- A checklist
- Practice
- Conclusion
-
Java Basics
- Introduction to Java
- Setting Up Java and Writing Your First Program
- Variables, Data Types and Literals in Java
- Type Casting and Type Conversion in Java
- Operators and Expressions in Java
- Input and Output in Java
- Comments, Keywords and Naming Conventions in Java
- Control Flow in Java: if, else and switch
- Loops in Java: for, while and do-while
- Methods
- Arrays and Strings
-
OOP
- Classes and Objects in Java
- Constructors in Java
- The this Keyword in Java
- The static Keyword in Java
- Encapsulation in Java
- Access Modifiers in Java
- Inheritance in Java
- Method Overriding and super in Java
- Polymorphism in Java
- Abstraction, Abstract Classes and Interfaces in Java
- Composition, Aggregation and Association in Java
- The Object Lifecycle in Java
- Core Java
- Exception Handling
-
Collections
- The Java Collections Framework
- List in Java: ArrayList, LinkedList, Vector and Stack
- Set in Java: HashSet, LinkedHashSet and TreeSet
- Map in Java: HashMap, LinkedHashMap and TreeMap
- How HashMap Works Internally in Java
- Queue and Deque in Java: ArrayDeque and PriorityQueue
- Iterators in Java
- Comparable and Comparator in Java
- Collections Utilities and Choosing the Right Collection
- Generics
- Java 8+
- Stream API
- Date and Time
- File and I/O
-
Multithreading
- Threads in Java: Processes, Runnable and Thread
- Thread Lifecycle in Java
- Synchronization in Java: synchronized and volatile
- Locks and Atomic Classes in Java
- Race Conditions and Deadlocks in Java
- The Executor Framework and Thread Pools in Java
- Future and CompletableFuture in Java
- Concurrent Collections in Java
- The Java Memory Model
- JVM and Memory
- Advanced Java
- Networking
- JDBC
- Testing
Never swallow an exception
// The worst line of code in any Java project
try {
save(note);
} catch (Exception e) {
// nothing
}The failure happened, nobody knows, and the symptom will appear somewhere unrelated. If an exception truly can be ignored, say so explicitly and explain why:
try {
Files.deleteIfExists(temporaryFile);
} catch (IOException e) {
logger.fine("Temporary file left behind, harmless: " + e.getMessage());
}Catch what you can handle
// Too broad: also catches bugs you did not anticipate
try {
process(row);
} catch (Exception e) { skip(row); }
// Precise: only the parsing failure is treated as a skippable row
try {
process(row);
} catch (NumberFormatException | DateTimeParseException e) {
skip(row);
}Fail fast at the boundary
public Report generate(String userId, LocalDate from, LocalDate to) {
Objects.requireNonNull(userId, "userId is required");
if (to.isBefore(from)) {
throw new IllegalArgumentException("to must not be before from");
}
// the rest of the method can now trust its inputs
}Validate once, at the edge. Code deeper inside then does not need defensive checks, and a bad input is reported where it entered rather than where it eventually breaks something.
Do not use exceptions for control flow
// Expensive and misleading
try {
return map.get(key).toString();
} catch (NullPointerException e) {
return "unknown";
}
// Clear and cheap
return map.getOrDefault(key, "unknown").toString();An exception should describe something exceptional. A missing key, an empty list or a value that is simply absent are ordinary outcomes, and Optional, a default value or a boolean return says so better.
Write messages that help
throw new IllegalArgumentException("Invalid input"); // useless
throw new IllegalArgumentException(
"pageSize must be between 1 and 100, was " + pageSize); // actionableA good message names the parameter, the rule and the actual value. Never include passwords, tokens or personal data, because messages end up in logs.
Always preserve the cause
catch (SQLException e) {
throw new StorageException("Save failed for note " + id); // trace lost
}
catch (SQLException e) {
throw new StorageException("Save failed for note " + id, e); // trace kept
}Log or rethrow, not both
// Produces the same failure in the log several times over
catch (IOException e) {
logger.severe("Failed: " + e.getMessage());
throw new UncheckedIOException(e);
}Handle it where you log it, or let it travel to the layer that will. Doing both at every level fills the log with duplicates of one event.
Use try with resources
try (var connection = pool.get();
var statement = connection.prepareStatement(sql)) {
return statement.executeQuery();
}Do not catch what you cannot fix
- Never catch
Errorto continue. - Never catch
NullPointerExceptionin place of a null check. - Never catch
Throwableexcept in a top level handler that logs and shuts down.
Clean up in the right place
// Interrupt handling is easy to get wrong
try {
queue.take();
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // restore the flag before returning
throw new IllegalStateException("Interrupted while waiting", e);
}Catching InterruptedException clears the interrupt flag. Restore it, or the code above you never learns that cancellation was requested.
Keep the try block small
// Which of these forty lines threw?
try {
// forty lines
} catch (Exception e) { }
// Only the risky call is guarded
String raw = readInput();
int value;
try {
value = Integer.parseInt(raw);
} catch (NumberFormatException e) {
value = 0;
}
process(value);A checklist
| Do | Avoid |
|---|---|
| Catch specific types | catch (Exception e) by default |
| Include the value in the message | Generic messages |
| Pass the cause when wrapping | Dropping the original |
| Use try with resources | Manual close() in finally |
| Validate at the boundary | Defensive checks everywhere |
Return Optional for absence | Exceptions for normal outcomes |
| Restore the interrupt flag | Swallowing InterruptedException |
| Handle once, at the right layer | Log and rethrow at every level |
Practice
- Find an empty
catchblock in code you have written and decide what it should do instead. - Rewrite an exception driven "key missing" check using
getOrDefault. - Improve a message such as "error occurred" so that it names the field, the rule and the value.
- Explain why catching
InterruptedExceptionwithout restoring the flag is a bug. - Take a method with one large
tryblock and narrow it to the statements that can actually fail.
Conclusion
Good exception handling is mostly restraint: catch only what you can handle, at the layer that can decide, with a message and a cause that make the failure obvious. Everything else should be allowed to travel.