Tuesday

Poor Exception Handling: 5 Mistakes Java Developers Keep Making

Poor Exception Handling: 5 Mistakes Java Developers Keep Making

Exception handling is one of the first things Java developers learn, yet it is also one of the easiest areas to get wrong.

Most developers focus heavily on the happy path: implementing features, writing business logic, and getting tests to pass. But production systems rarely fail because of the happy path. They fail because unexpected situations weren't handled properly.

After reviewing countless Java codebases over the years, I've noticed the same exception handling mistakes appearing again and again. These mistakes make applications harder to debug, harder to maintain, and more likely to cause production incidents.

Let's look at five common mistakes and how experienced Java developers handle them.


1. Catching Generic Exceptions

One of the most common anti-patterns is catching the generic Exception class.

❌ Bad

try {
    processOrder();
} catch (Exception e) {
    logger.error("Something went wrong");
}

At first glance, this might seem convenient. Unfortunately, it creates several problems:

  • It hides the real issue.
  • It makes troubleshooting more difficult.
  • It catches exceptions you may not have intended to handle.
  • It reduces the clarity of your code.

✅ Better

try {
    processOrder();
} catch (OrderValidationException e) {
    logger.warn("Invalid order", e);
}

By catching specific exceptions, your code communicates intent more clearly and makes failures easier to diagnose.

Senior Takeaway: Catch the most specific exception possible.

2. Swallowing Exceptions

Another dangerous practice is catching an exception and doing absolutely nothing with it.

❌ Bad

try {
    sendEmail();
} catch (MessagingException e) {
    // ignore
}

This is often referred to as "swallowing" an exception.

When this happens:

  • Failures disappear silently.
  • Production issues become difficult to identify.
  • Monitoring systems have no visibility into problems.
  • Users may experience failures with no explanation.

✅ Better

try {
    sendEmail();
} catch (MessagingException e) {
    logger.error("Failed to send email", e);
    throw new NotificationException(
        "Unable to send email", e
    );
}

This version logs the failure and propagates meaningful context to the caller.

Senior Takeaway: Every exception should be handled, logged, or propagated.

3. Losing the Original Stack Trace

Many developers wrap exceptions without preserving the original cause.

❌ Bad

try {
    repository.save(user);
} catch (SQLException e) {
    throw new RuntimeException("Database error");
}

The issue is subtle but significant.

The original SQLException is discarded, along with the valuable stack trace that explains what actually went wrong.

✅ Better

try {
    repository.save(user);
} catch (SQLException e) {
    throw new RuntimeException(
        "Database error", e
    );
}

Now the original exception is preserved as the cause.

When someone investigates a production issue months later, they'll thank you.

Senior Takeaway: Always preserve the root cause.

4. Using Exceptions for Normal Control Flow

Exceptions are meant for exceptional situations, not routine business logic.

❌ Bad

try {
    Integer.parseInt(input);
} catch (NumberFormatException e) {
    return false;
}

While this works, it uses an exception to control expected behavior.

Problems include:

  • Reduced readability.
  • Unnecessary object creation.
  • Performance overhead.
  • Business logic becoming harder to understand.

✅ Better

return input.matches("-?\\d+");

In many situations, validating data before processing it is a cleaner approach. If you also need the parsed value, prefer a method that returns a result instead of throwing, so the exception no longer drives normal control flow.

Senior Takeaway: Exceptions should represent exceptional situations.

5. Exposing Internal Errors to API Consumers

Many applications accidentally expose technical details directly to users or API consumers.

❌ Bad

@GetMapping("/users/{id}")
public User getUser(Long id) {
    return service.findUser(id);
}

This can lead to responses like:

{
  "exception": "NullPointerException",
  "stackTrace": "..."
}

This creates several issues:

  • Security risks
  • Information leakage
  • Poor user experience
  • Unprofessional APIs

Users don't care about your stack traces. They care about understanding what went wrong.

✅ Better

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<ErrorResponse> handle() {

        return ResponseEntity.status(404)
            .body(new ErrorResponse("User not found"));
    }
}

This approach provides a clean, meaningful response while hiding implementation details.

Senior Takeaway: Return business-friendly errors, not technical internals.

What Senior Java Developers Do Differently

As developers gain experience, their approach to exception handling evolves.

  • ✅ Use custom exceptions where appropriate
  • ✅ Preserve root causes when wrapping exceptions
  • ✅ Log errors at the correct severity level
  • ✅ Centralize exception handling
  • ✅ Return clean API responses
  • ✅ Avoid empty catch blocks
  • ✅ Fail fast when necessary
  • ✅ Treat error handling as part of application design, not an afterthought

Final Thoughts

Good exception handling doesn't just prevent crashes.

It makes software easier to debug, easier to maintain, and easier to operate in production.

The difference between junior and senior Java code is often not visible in the happy path. Most code looks good when everything works. The real difference appears when things go wrong.

The next time you review your code, pay special attention to how failures are handled. You may discover that improving your exception handling has a bigger impact on code quality than adding another feature.

Remember: Great developers don't just write code that works, they write code that fails gracefully.