In Week 5, we secured our Spring Boot APIs using JWT authentication and role-based access.
Now, in Week 6, we’ll focus on handling errors gracefully and implementing professional logging — two crucial aspects of production-ready applications.
By the end of this article, your application will catch errors centrally, provide clean API responses, and have a robust logging system for debugging and monitoring.
Why Error Handling & Logging Matter
❌ Without proper error handling:
- APIs return messy stack traces
- Debugging is difficult
- User experience suffers
❌ Without logging:
- Hard to monitor production issues
- No insights into user behavior
- Difficult to troubleshoot crashes
✅ Proper handling and logging provide:
- Centralized error management
- Consistent API responses
- Easy debugging and monitoring
- Professional, maintainable code
Step 1: Global Exception Handling with @ControllerAdvice
Spring Boot allows centralized exception handling using @ControllerAdvice:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ApiError> handleResourceNotFound(ResourceNotFoundException ex) {
ApiError error = new ApiError(HttpStatus.NOT_FOUND, ex.getMessage());
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiError> handleValidationException(MethodArgumentNotValidException ex) {
String message = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.joining(", "));
ApiError error = new ApiError(HttpStatus.BAD_REQUEST, message);
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiError> handleGenericException(Exception ex) {
ApiError error = new ApiError(HttpStatus.INTERNAL_SERVER_ERROR, "Something went wrong");
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
}ApiError Class
public class ApiError {
private int status;
private String message;
private LocalDateTime timestamp;
public ApiError(HttpStatus status, String message) {
this.status = status.value();
this.message = message;
this.timestamp = LocalDateTime.now();
}
// Getters & Setters
}✅ Benefits:
- Consistent error response structure
- Easy for frontend to parse
- Centralized handling reduces boilerplate
Step 2: Custom Exceptions
Create domain-specific exceptions:
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}Usage in Service:
User user = userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("User not found with ID " + id));Step 3: Logging Setup
Spring Boot uses SLF4J + Logback by default.
You can customize logging in application.yml:
logging:
level:
root: INFO
com.example.demo: DEBUG
file:
name: logs/app.log- root → default logging level
- package-specific → fine-grained logs
- file.name → log to file for production
Step 4: Logging in Code
@RestController
@RequestMapping("/api/users")
public class UserController {
private static final Logger logger = LoggerFactory.getLogger(UserController.class);
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
logger.info("Fetching user with ID: {}", id);
User user = userService.getUserById(id)
.orElseThrow(() -> new ResourceNotFoundException("User not found"));
logger.debug("User details: {}", user);
return ResponseEntity.ok(user);
}
}Logging Best Practices:
- Use info for general events
- Use debug for detailed dev info
- Use error for exceptions
Step 5: Structured Logging (Optional for Production)
Use JSON logging for modern applications:
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<timestamp>
<timeZone>UTC</timeZone>
</timestamp>
<loggerName />
<level />
<threadName />
<message />
</providers>
</encoder>- Easier for ELK / Grafana / Kibana dashboards
- Perfect for microservices and cloud deployments
Step 6: Combining Error Handling + Logging
Centralized exception handler + logging:
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiError> handleGenericException(Exception ex) {
logger.error("Unexpected error occurred: ", ex);
ApiError error = new ApiError(HttpStatus.INTERNAL_SERVER_ERROR, "Something went wrong");
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
}- Every exception is logged
- Users get clean messages
Step 7: Validation Error Logging Example
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiError> handleValidationException(MethodArgumentNotValidException ex) {
String message = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(err -> err.getField() + ": " + err.getDefaultMessage())
.collect(Collectors.joining(", "));
logger.warn("Validation failed: {}", message);
ApiError error = new ApiError(HttpStatus.BAD_REQUEST, message);
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}✅ Developers immediately see which fields failed without crashing the app.
Week 6 Recap
✅ Centralized exception handling with @ControllerAdvice
✅ Custom exceptions for domain logic
✅ Logging with SLF4J / Logback
✅ Logging levels: info, debug, warn, error
✅ Structured logging for production
✅ Combining exception handling + logging for maintainable APIs
What’s Next? (Week 7)
👉 Spring Boot Testing: Unit, Integration & Performance
We’ll cover:
- JUnit 5 + Mockito
- Integration testing with Spring Boot
- TestContainers for database
- API testing
- Performance tips
📌 Action Items
✔ Implement @ControllerAdvice
✔ Add custom exceptions for your entities
✔ Add logging to all controllers and services
✔ Configure logging levels in application.yml
✔ Test logging + error responses in Postman