Error Handling Best Practices in Spring Boot Applications
Erik Nguyen / December 11, 2024
Error Handling Best Practices in Spring Boot Applications
Proper error handling is crucial for building robust and maintainable Spring Boot applications. Well-implemented error handling not only improves the developer experience but also helps end-users better understand and resolve issues. In this comprehensive guide, we'll explore best practices for handling errors in Spring Boot applications.
Never expose sensitive information in error responses, such as stack traces, database details, or internal system configurations. This could potentially expose security vulnerabilities in your application.
Understanding Spring Boot's Default Error Handling
Spring Boot provides a default error handling mechanism through its ErrorController
implementation. However, for production applications, you'll want to customize this to provide more meaningful error responses.
Creating a Global Exception Handler
The first step in implementing custom error handling is creating a global exception handler:
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleResourceNotFoundException(
ResourceNotFoundException ex, WebRequest request) {
ErrorResponse errorResponse = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
ex.getMessage(),
request.getDescription(false)
);
log.error("Resource not found: {}", ex.getMessage());
return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleAllUncaughtException(
Exception ex, WebRequest request) {
ErrorResponse errorResponse = new ErrorResponse(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
"An unexpected error occurred",
request.getDescription(false)
);
log.error("Unexpected error occurred", ex);
return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
Standardizing Error Responses
Create a consistent error response structure:
@Getter
@Setter
public class ErrorResponse {
private LocalDateTime timestamp;
private int status;
private String message;
private String path;
private List<String> errors;
public ErrorResponse(int status, String message, String path) {
this.timestamp = LocalDateTime.now();
this.status = status;
this.message = message;
this.path = path;
this.errors = new ArrayList<>();
}
}
Custom Exception Classes
Define custom exceptions for different types of business errors:
@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
public class BusinessValidationException extends RuntimeException {
private final List<String> errors;
public BusinessValidationException(String message, List<String> errors) {
super(message);
this.errors = errors;
}
}
Handling Validation Errors
Spring Boot provides built-in support for validation. Here's how to handle validation errors effectively:
@ControllerAdvice
public class ValidationExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationExceptions(
MethodArgumentNotValidException ex, WebRequest request) {
List<String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.toList());
ErrorResponse errorResponse = new ErrorResponse(
HttpStatus.BAD_REQUEST.value(),
"Validation failed",
request.getDescription(false)
);
errorResponse.setErrors(errors);
return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}
}
Implementation Example
Here's how to use these error handling components in a REST controller:
@RestController
@RequestMapping("/api/users")
@Validated
public class UserController {
private final UserService userService;
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUser(@PathVariable Long id) {
try {
UserDTO user = userService.getUserById(id);
return ResponseEntity.ok(user);
} catch (ResourceNotFoundException ex) {
throw ex; // Will be handled by GlobalExceptionHandler
}
}
@PostMapping
public ResponseEntity<UserDTO> createUser(
@Valid @RequestBody UserCreateRequest request) {
// Validation errors will be handled by ValidationExceptionHandler
UserDTO user = userService.createUser(request);
return new ResponseEntity<>(user, HttpStatus.CREATED);
}
}
Always log errors appropriately based on their severity level. Use ERROR for unexpected errors, WARN for business validation failures, and DEBUG for validation errors.
Best Practices for Error Handling
1. Use Hierarchy in Exception Handling
Create a hierarchy of exceptions to handle different types of errors:
public abstract class BaseException extends RuntimeException {
private final String errorCode;
protected BaseException(String message, String errorCode) {
super(message);
this.errorCode = errorCode;
}
}
public class DatabaseException extends BaseException {
public DatabaseException(String message) {
super(message, "DB_ERROR");
}
}
2. Include Error Context
Provide meaningful context in error messages:
throw new ResourceNotFoundException(
String.format("User with ID %d not found in organization %d", userId, orgId)
);
3. Implement Request Tracing
Add request tracing for better error tracking:
@Component
public class RequestTraceInterceptor implements HandlerInterceptor {
private static final String TRACE_ID = "X-Trace-ID";
@Override
public boolean preHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler) {
String traceId = UUID.randomUUID().toString();
MDC.put(TRACE_ID, traceId);
response.setHeader(TRACE_ID, traceId);
return true;
}
}
Testing Error Handlers
Don't forget to write tests for your error handlers:
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void whenResourceNotFound_thenReturns404() throws Exception {
mockMvc.perform(get("/api/users/999"))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.status").value(404))
.andExpect(jsonPath("$.message").value("User with ID 999 not found"));
}
}
Conclusion
Implementing proper error handling in Spring Boot applications is crucial for building robust and maintainable systems. By following these best practices, you can:
- Provide consistent and meaningful error responses
- Protect sensitive information
- Improve debugging and troubleshooting
- Enhance the overall user experience
Remember to:
- Use global exception handlers
- Create custom exceptions for business cases
- Implement proper logging
- Include request tracing
- Write comprehensive tests
With these implementations in place, your application will be better equipped to handle errors gracefully and provide meaningful feedback to both users and developers.