When we build REST APIs using Spring Boot, one thing is guaranteed — things will go wrong.
A user may send invalid data, a record might not exist in the database, or some unexpected error may occur on the server. If we don’t handle these situations properly, our API may return confusing error messages or even expose internal details (which is bad for security).
That’s where exception handling comes into play.
In this blog, we’ll understand:
- Why exception handling is important
- Different ways to handle exceptions in Spring Boot
- How to create clean and consistent error responses
- Best practices for production-ready APIs
Why Exception Handling Matters in REST APIs
Imagine calling an API and getting this response:
{
"timestamp": "2025-01-01T10:30:00",
"status": 500,
"error": "Internal Server Error",
"path": "/api/users/10"
}
As a client, this doesn’t help much.
Now compare it with this:
{
"status": 404,
"message": "User with id 10 not found",
"path": "/api/users/10"
}
Clear, meaningful, and user-friendly. Good exception handling helps you:
- Return proper HTTP status codes
- Send meaningful error messages
- Keep your API consistent
- Improve debugging and maintainability
Common Exceptions in REST APIs
Some common scenarios:
- 400 Bad Request – Invalid input
- 404 Not Found – Resource doesn’t exist
- 401 Unauthorized – Authentication required
- 403 Forbidden – No permission
- 500 Internal Server Error – Something went wrong on server
Basic Example: Throwing an Exception
Let’s say we have a simple UserController.
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("User not found"));
}
This works, but returning a generic RuntimeException is not a good practice.
Creating a Custom Exception
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}
Now use it in your controller or service:
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found with id: " + id));
}
Handling Exceptions Using @ExceptionHandler
Spring allows us to handle exceptions using @ExceptionHandler.
@RestController
public class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<String> handleUserNotFound(UserNotFoundException ex) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(ex.getMessage());
}
}
This works, but it’s still not scalable when your application grows.
Best Approach: @RestControllerAdvice
@RestControllerAdvicepublic class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException.class) public ResponseEntity<ErrorResponse> handleUserNotFound(UserNotFoundException ex, HttpServletRequest request) { ErrorResponse error = new ErrorResponse( HttpStatus.NOT_FOUND.value(), ex.getMessage(), request.getRequestURI() ); return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); }}
Creating a Standard Error Response Model
public class ErrorResponse {
private int status;
private String message;
private String path;
public ErrorResponse(int status, String message, String path) {
this.status = status;
this.message = message;
this.path = path;
}
// getters and setters
}
Sample API Response
{
"status": 404,
"message": "User not found with id: 10",
"path": "/api/users/10"
}
Handling Validation Errors (@Valid)
public class UserRequest {
@NotBlank(message = "Name is mandatory")
private String name;
@Email(message = "Invalid email format")
private String email;
}
Controller
@PostMapping("/users")
public ResponseEntity<String> createUser(@Valid @RequestBody UserRequest request) {
return ResponseEntity.ok("User created successfully");
}
Handling Validation Exception
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationErrors(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors()
.forEach(error -> errors.put(error.getField(), error.getDefaultMessage()));
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(errors);
}
Sample Response
{
"name": "Name is mandatory",
"email": "Invalid email format"
}
Handling Generic Exceptions
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception ex,
HttpServletRequest request) {
ErrorResponse error = new ErrorResponse(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
"Something went wrong. Please try again later.",
request.getRequestURI()
);
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
Best Practices for Exception Handling
- Use custom exceptions
- Use @RestControllerAdvice
- Return proper HTTP status codes
- Never expose internal stack traces
- Keep error responses consistent
- Log exceptions internally (using SLF4J)