Exceptions are a normal part of backend development, but inconsistent exception handling can make a REST API difficult to use and diagnose. A well-designed service distinguishes expected business failures from unexpected technical problems and communicates both through appropriate HTTP responses.
Distinguish business failures from technical failures
A business failure might occur when a requested order does not exist or an operation violates a domain rule. A technical failure might occur when a database is unavailable or an unexpected programming error occurs.
These cases should not all produce the same response.
Define meaningful exception types for expected conditions:
public class OrderNotFoundException extends RuntimeException {
public OrderNotFoundException(long orderId) {
super("Order not found: " + orderId);
}
}
A service can throw this exception when an order cannot be found:
public Order getOrder(long orderId) {
return orderRepository.findById(orderId)
.orElseThrow(() ->
new OrderNotFoundException(orderId));
}
The service expresses the business condition without deciding how the HTTP response must be formatted.
Centralize exception handling
In Spring Boot, @RestControllerAdvice can map exceptions to consistent API responses:
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(OrderNotFoundException.class)
public ResponseEntity<ProblemDetail> handleNotFound(
OrderNotFoundException exception) {
ProblemDetail problem =
ProblemDetail.forStatus(HttpStatus.NOT_FOUND);
problem.setTitle("Order not found");
problem.setDetail(exception.getMessage());
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(problem);
}
}
This example uses Spring’s ProblemDetail support, available in modern Spring Framework versions. The exact configuration depends on the application’s Spring version and error-handling requirements.
Centralization prevents every controller from inventing its own response format.
Choose appropriate HTTP status codes
Common mappings include:
- 400 Bad Request for invalid request data.
- 401 Unauthorized for missing or invalid authentication.
- 403 Forbidden when the caller lacks permission.
- 404 Not Found when a resource does not exist.
- 409 Conflict when a request conflicts with current resource state.
- 500 Internal Server Error for unexpected server failures.
Status codes should describe the actual condition. Avoid returning 200 OK when an operation failed.
Do not swallow exceptions
A broad catch block that hides an error can make a service appear successful when it has actually failed:
try {
paymentService.process(payment);
} catch (Exception exception) {
// Ignoring the failure is dangerous.
}
If an exception can be handled meaningfully, handle it explicitly. Otherwise, allow it to propagate to a suitable boundary where it can be logged and translated into an appropriate response.
Log failures without exposing internals
Logs should include useful context, such as an operation identifier or correlation ID, while avoiding passwords, access tokens, and sensitive personal data.
Client responses should not expose stack traces, database connection strings, or internal implementation details. An unexpected exception should generally produce a generic error message while the full diagnostic information remains available to authorized operators.
Handle transactions and external calls carefully
An exception can affect transaction behavior. Spring’s transaction management has specific rollback rules, so developers should understand which exceptions trigger rollback in their configuration.
External HTTP calls also need timeouts and clear failure handling. Retrying an operation without considering whether it is safe to repeat can cause duplicate side effects.
Conclusion
Good exception handling makes REST services predictable and maintainable. Define meaningful business exceptions, centralize HTTP error mapping, use appropriate status codes, and preserve diagnostic information without exposing internals. The goal is to make failures understandable without hiding them.