Exception best practices in Java
Never swallow, fail fast, don't use exceptions for flow control, preserve the cause.
{ }. Can you guess where?Rule 1: never swallow
An empty catch block hides the failure: the program continues in a bad state and nobody knows why. Every catch should log, recover or rethrow. If you truly can't handle it here, don't catch it.
The silent catch
try {
saveOrder(order);
} catch (IOException e) {
}
sendConfirmationEmail(order);Saving fails silently, and the email goes out anyway.
try {
saveOrder(order);
} catch (IOException e) {
throw new OrderException("save", e);
}
sendConfirmationEmail(order);Failure stops the flow before the email, with the cause kept.
Rule 2: fail fast
Validate input as early as possible, with a clear message: Objects.requireNonNull(x, "x") throws NullPointerException right away; bad values get IllegalArgumentException. A bad object never gets the chance to cause a confusing failure later.
record User(String name) {
User {
Objects.requireNonNull(name, "name");
}
}Stopped at the door
What happens?
record Email(String value) {
Email {
Objects.requireNonNull(value, "value");
}
}
void main() {
var e = new Email(null);
System.out.println(e);
}Email[value=null]Throws NullPointerExceptionThrows IllegalArgumentException
Show the answer
requireNonNull fails fast at construction with the message value, so an invalid Email can never exist.
Rule 3: exceptions aren't a loop condition
int i = 0;
try {
while (true) process(items[i++]);
} catch (ArrayIndexOutOfBoundsException e) {
}Slow, and a real index bug inside process() is swallowed too.
for (var item : items) {
process(item);
}Reaching the end of an array is expected — use plain control flow.
Rule 4: keep the cause
A DAO catches SQLException and throws new RepositoryException("save failed"). Logs show only "save failed". What's the fix?
Think about it, then reveal the answer
Pass the cause: **new RepositoryException("save failed", e). Wrapping is fine — but without the cause, the root error and its stack trace are thrown away. And remember: catch only what you can actually handle.**
Postmortem favourites
Swallowed exceptions are a top cause of "it failed silently for three weeks" incidents. That's why tools like SonarQube flag empty catch blocks, and why many teams forbid them in code review.
Key takeaways
- No empty catch blocks — log, recover or rethrow
- Fail fast: Objects.requireNonNull, IllegalArgumentException
- Use if-checks, not exceptions, for expected conditions
- Wrap with the cause: new X(msg, e)
💡 A smoke alarm with the battery removed is quiet — right up until the house burns down.
Objects.requireNonNull arrived in Java 7; Java 9 added requireNonNullElse(obj, fallback) for cases where a default value makes more sense than failing.
Practice questions
This loop ends by catching an exception. Best fix?
int i = 0;
try {
while (true) {
process(items[i++]);
}
} catch (ArrayIndexOutOfBoundsException e) { }- for (var item : items) process(item);
- Catch Exception instead of ArrayIndexOutOfBoundsException
- Add a finally block
- Log the exception inside the catch
Check your answer
for (var item : items) process(item);. Reaching the end of an array is expected, so use a normal loop. Exceptions for control flow are slow and hide real bugs.
What does this print?
record User(String name) {
User {
Objects.requireNonNull(name, "name");
}
}
void main() {
var u = new User(null);
System.out.println(u);
}- User[name=null]
- Throws NullPointerException
- Throws IllegalArgumentException
- null
Check your answer
Throws NullPointerException. requireNonNull fails fast at construction, with a clear message, instead of letting a bad User cause a confusing failure later.