💥 Exceptions & Errors · Intermediate

Exception best practices in Java

Never swallow, fail fast, don't use exceptions for flow control, preserve the cause.

🧩 The mysteryA customer gets an order confirmation email — but their order was never saved. The bug is two characters long: { }. 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

✗ Swallowed
try {
    saveOrder(order);
} catch (IOException e) {
}
sendConfirmationEmail(order);

Saving fails silently, and the email goes out anyway.

✓ Loud and safe
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");
    }
}
🔮 Predict it

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);
}
  1. Email[value=null]
  2. Throws NullPointerException
  3. Throws 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

✗ Flow by exception
int i = 0;
try {
    while (true) process(items[i++]);
} catch (ArrayIndexOutOfBoundsException e) {
}

Slow, and a real index bug inside process() is swallowed too.

✓ Normal loop
for (var item : items) {
    process(item);
}

Reaching the end of an array is expected — use plain control flow.

🤔 Think first

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.**

💼 In the real world

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

  1. No empty catch blocks — log, recover or rethrow
  2. Fail fast: Objects.requireNonNull, IllegalArgumentException
  3. Use if-checks, not exceptions, for expected conditions
  4. Wrap with the cause: new X(msg, e)

💡 A smoke alarm with the battery removed is quiet — right up until the house burns down.

🤯 Did you know?

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) { }
  1. for (var item : items) process(item);
  2. Catch Exception instead of ArrayIndexOutOfBoundsException
  3. Add a finally block
  4. 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);
}
  1. User[name=null]
  2. Throws NullPointerException
  3. Throws IllegalArgumentException
  4. 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.

Next: a statement that checks your assumptions during testing… and vanishes completely in production. Meet assert.