Exception Handling Best Practices
Exceptions are only as good as the way you use them. Handled well, they make failures obvious, local and easy to fix. Handled badly, they hide bugs, lose the real cause, and turn a two-minute fix into a day of searching. This lesson collects the habits experienced Java developers follow, each with the reason behind it, and ends with a small before-and-after.
The Habits
| # | Habit | Why |
|---|---|---|
| 1 | Validate early, fail fast | A bad value rejected at the start, with a clear message, is far easier to trace than a strange failure ten calls later |
| 2 | Catch specific exceptions | catch (Exception e) also catches bugs you didn't mean to handle |
| 3 | Never swallow an exception | An empty catch makes failures silent; at minimum, log it |
| 4 | Catch where you can act | Low-level code rarely knows the right response; let exceptions propagate to code that does |
| 5 | Keep the cause when wrapping | new MyException("msg", e) preserves the original stack trace |
| 6 | Use try-with-resources | Resources are always closed, and closing errors don't hide the real one |
| 7 | Write useful messages | Say what failed and include the bad value: "Deposit must be positive, got -200" |
| 8 | Don't use exceptions for normal flow | Throwing costs time (the stack trace is captured); an if is clearer and faster |
| 9 | Prefer standard exceptions | IllegalArgumentException, IllegalStateException and friends are understood instantly |
| 10 | Don't catch Throwable or Error | You'd also catch OutOfMemoryError, which you can't fix by catching |
| 11 | Log once, at the boundary | Logging and rethrowing at every level prints the same error many times |
| 12 | Never return from finally | It discards the try's result and any exception |
Before and After
public class Before {
static int parse(String s) {
try {
return Integer.parseInt(s);
} catch (Exception e) {
return 0; // swallowed, and hides real bugs
}
}
}public class After {
static int parseQuantity(String text) {
if (text == null || text.isBlank()) {
throw new IllegalArgumentException("Quantity is required");
}
try {
return Integer.parseInt(text.strip());
} catch (NumberFormatException e) {
throw new IllegalArgumentException(
"Quantity must be a whole number, got \"" + text + "\"", e);
}
}
public static void main(String[] args) {
for (String in : new String[] {" 3 ", "three", ""}) {
try {
System.out.println("Advitha ordered " + parseQuantity(in));
} catch (IllegalArgumentException e) {
System.out.println("Rejected: " + e.getMessage());
}
}
}
}Advitha ordered 3
Rejected: Quantity must be a whole number, got "three"
Rejected: Quantity is requiredWhat changed
- Fail fast: blank input is rejected at the start with its own message.
- Specific catch: only
NumberFormatExceptionis caught, so unrelated bugs still surface. - No swallowing: instead of a silent
0, the caller gets a clear exception it can show to the user. - Cause kept: the original
NumberFormatExceptionis attached. - Useful message: it says what was expected and what arrived.
Exceptions Cost Time
Creating an exception records the whole call stack, which is relatively slow. In one run of 200,000 throws, creating normal exceptions took about 70 ms, while the same loop with a plain if check took under 1 ms. That's nothing for real errors, but it's a reason not to use exceptions as ordinary control flow, such as ending a loop.
Common Mistakes
e.printStackTrace() left in production code. Use a logger, which records time, thread and context.
throws Exception everywhere. Declare the specific checked exceptions.
Catching and returning null. The caller then fails later with a NullPointerException far from the real cause.
Interview Corner
What are some exception handling best practices in Java?
Validate inputs early and fail fast with clear messages; catch the most specific exception types, and only where you can respond meaningfully; never leave catch blocks empty; preserve the original exception as the cause when wrapping; use try-with-resources for anything that must be closed; prefer standard exception types; don't use exceptions for normal control flow; don't catch Throwable or Error; log an exception once, at the boundary where it's handled; and never return from a finally block.
Practice
- Easy: Find the problems in this code and rewrite it:
try { save(order); } catch (Exception e) { }. - Harder: Rewrite a method that returns
-1for "not found" so that it either returns anOptionalor throws a custom exception, and explain which you'd choose and why.
Key Takeaways
Was this tutorial helpful?