Modern Java Exception Handling: Best Practices and Logging Strategies
-
Jason Yang - 29 Nov, 2025
- Updated 04 Jan, 2026
- Views —
How senior developers handle errors in real production systems
Exception handling is one of the least glamorous topics in software development—yet it is the backbone of reliable, maintainable systems. In the previous post, we discussed why modern Java development favors unchecked exceptions for business logic.
In this follow-up, we’ll look at how to handle, log, and organize those exceptions effectively in real-world Spring Boot applications.
1. Exception Handling Best Practices
✔ 1) Handle exceptions at the correct layer
Not every layer should handle every error:
| Layer | Responsibility |
|---|---|
| Controller | Delegate to services; minimal handling |
| Service | Throw meaningful business exceptions |
| Repository | Wrap low-level exceptions |
| Global Handler (@ControllerAdvice) | Logging + final API response |
Rule of thumb:
Handle locally only if the issue is recoverable.
Otherwise, propagate upward and let your global handler manage the response.
✔ 2) Do not catch Error
Error represents JVM-level failures (OutOfMemoryError, StackOverflowError).
Catching them is almost always wrong.
✔ 3) Provide meaningful messages
Good exception messages explain:
- What happened
- Why it happened
- What the user or system can do
Bad:
throw new RuntimeException("Failed");
Good:
throw new FileStorageException("Failed to store file: " + filename);
✔ 4) Wrap and chain exceptions properly
When wrapping checked exceptions, always preserve the original cause.
catch (IOException e) {
throw new FileStorageException("Failed to store file: " + filename, e);
}
This keeps the full stack trace and root cause for debugging.
✔ 5) Use try-with-resources for all I/O
Avoid manual close() calls.
try (var input = Files.newInputStream(path)) {
return input.readAllBytes();
}
2. Logging Strategy (Real-World, Production Grade)
✔ Log exceptions exactly once
Double logging is one of the worst anti-patterns.
❌ Bad (double-logging everywhere)
- Repository logs
- Service logs
- Controller logs
- Global handler logs
This floods the system with repeated traces.
✔ Good:
- Log once at the system boundary (usually the global handler)
- Internal layers throw, not log
✔ Avoid “log + rethrow” unless you’re certain
Bad:
catch (Exception e) {
log.error("Error saving user", e);
throw e; // ❌ Global handler logs again → duplicates
}
✔ Include context information
A log without context is noise.
Good logging example:
log.error("Failed to process order. orderId={}, userId={}", orderId, userId, e);
Recommended context fields:
- userId
- orderId
- filename
- clientIp
- transactionId
- correlationId
3. Practical Q&A (Real-World Scenarios)
Q1. Should all custom exceptions extend RuntimeException?
Short answer: Usually yes, but not always.
Use RuntimeException for:
- Business rule violations
- Domain-level errors
- Validation logic
Use Checked Exceptions for:
- External I/O
- Filesystem, network, DB drivers
- Recoverable or interruptible operations
Q2. Where should exceptions be handled?
Best practice for Spring Boot:
| Layer | Strategy |
|---|---|
| Controller | No try-catch; delegate |
| Service | Throw meaningful custom exceptions |
| Repository | Wrap external/checked exceptions |
| Global | @ControllerAdvice handles everything |
The global exception handler is the single point for:
- Logging
- HTTP status mapping
- Returning standardized error responses
Q3. Should I wrap every exception inside one generic exception?
No.
You lose semantic meaning, and debugging becomes much harder.
Instead, design a clean exception hierarchy.
4. Final Thoughts
Mastering exception handling goes far beyond understanding Java’s hierarchy.
Senior developers focus on:
- Using checked exceptions only for external resources
- Using unchecked exceptions for business logic
- Designing meaningful custom exception classes
- Avoiding double logging
- Mapping errors cleanly with
@ControllerAdvice - Providing helpful, contextual messages
When done right, exceptions become a powerful tool for building clean, reliable, and production-ready applications.