Checked vs Unchecked Exceptions in Modern Microservices: Why Try-Catch Everywhere Is an Anti-Pattern
Exception handling is a fundamental part of application design, yet many systems — especially legacy enterprise codebases — still rely heavily on checked exceptions and extensive try-catch blocks across every layer.
In modern microservices architectures, this approach often causes more harm than good.
Let’s explore:
- The difference between checked and unchecked exceptions
- Why checked exceptions don’t scale well in distributed systems
- Why catching exceptions everywhere is an anti-pattern
- The modern Spring Boot approach to error handling
Checked vs Unchecked Exceptions in Java
Checked Exceptions
Checked exceptions must be explicitly handled or declared:
public void process() throws IOException {
}
Typical examples:
- IOException
- SQLException
They force developers to use try-catch or add throws everywhere.
Unchecked Exceptions (RuntimeException)
Unchecked exceptions extend RuntimeException:
public class PaymentFailedException extends RuntimeException {
}
They don’t require explicit handling at each call site.
Why Checked Exceptions Worked in Monoliths
In older monolithic systems:
- Most failures were local (DB, file system)
- Call stacks were short
- Recovery logic was sometimes possible
Checked exceptions helped enforce local handling.
But this world has changed.
Why Checked Exceptions Are a Poor Fit for Microservices
In distributed systems:
❌ Failures often come from remote services
❌ Recovery usually requires orchestration, not local handling
❌ Call chains span multiple services
Forcing each layer to handle technical failures leads to:
- Boilerplate try-catch blocks
- Duplicate logging
- Artificial exception wrapping
- Hard-to-read code
Example of a common anti-pattern:
try {
repository.save(entity);
} catch (SQLException e) {
log.error("DB error", e);
throw new ServiceException(e);
}
Repeated again in upper layers.
The Try-Catch Everywhere Anti-Pattern
Many enterprise systems look like this:
try {
service.process();
} catch (Exception e) {
log.error("Error occurred", e);
throw e;
}
in controllers, services, repositories, clients — everywhere.
Problems:
- Same exception logged multiple times
- Logs filled with noise
- Hard to identify real failure point
- Bloated code
This adds complexity without improving reliability.
The Modern Spring Boot Approach
1️⃣ Prefer Unchecked Exceptions
Use RuntimeException for:
- Business errors
- Technical failures
- Integration issues
Example:
public class OrderCreationException extends RuntimeException {
public OrderCreationException(String message) {
super(message);
}
}
2️⃣ Let Exceptions Propagate Naturally
Avoid catching unless you can:
✔ add meaningful context
✔ transform to a domain-specific exception
Bad:
catch (Exception e) {
throw e;
}
Good:
catch (PaymentClientException e) {
throw new PaymentFailedException(orderId, e);
}
3️⃣ Handle Centrally with @ControllerAdvice
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(OrderCreationException.class)
public ResponseEntity<?> handleOrderError(OrderCreationException ex) {
return ResponseEntity.badRequest().body(ex.getMessage());
}
@ExceptionHandler(Exception.class)
public ResponseEntity<?> handleUnexpected(Exception ex) {
return ResponseEntity.status(500).body("Internal server error");
}
}
Optionally log here or where business context exists.
When Does Catching Make Sense?
Use try-catch when:
✅ You can recover (retry, fallback)
✅ You need to translate low-level errors into domain errors
✅ You can enrich the exception with context
Example:
try {
paymentClient.charge(order);
} catch (TimeoutException ex) {
throw new PaymentServiceUnavailableException(order.getId(), ex);
}
Logging and Exceptions: A Clean Strategy
A good rule:
Either handle the exception or let it propagate — don’t log and rethrow everywhere.
Recommended:
- Log once with context
- Use centralized handling
Benefits of the Modern Approach
✔ Cleaner code
✔ Less boilerplate
✔ Better observability
✔ Easier maintenance
✔ Better fit for distributed systems
Final Thoughts
Checked exceptions and heavy try-catch usage belong mostly to legacy monolithic designs.
In modern Spring Boot microservices:
- Prefer unchecked exceptions
- Let them propagate
- Handle centrally
- Catch only when you add real value
This leads to simpler, more robust, and more maintainable systems.
Key Takeaways
- Checked exceptions don’t scale well in microservices
- Try-catch everywhere is an anti-pattern
- Use unchecked exceptions for most failures
- Centralize exception handling
- Catch only when necessary