Comprehensive Guide to Exception Handling in Spring Boot

Muhire JosuéMuhire Josué
4 min read

Introduction

Exception handling is a critical aspect of building robust Spring Boot applications. A well-structured error-handling mechanism improves maintainability, provides meaningful error messages, and enhances the user experience. In this guide, we will explore different approaches to handling exceptions in Spring Boot, from basic try-catch blocks to advanced techniques like global exception handling using @ControllerAdvice.

1. Understanding Exceptions in Spring Boot

Spring Boot applications interact with various components, including databases, REST APIs, and third-party services. Errors can arise from different sources, including:

  • Client Errors (400 series) – Invalid user input, unauthorized access, or bad requests.

  • Server Errors (500 series) – Internal application failures like database connection failures or unhandled null pointers.

  • Business Logic Errors – Application-specific rules violated during execution.

Handling these errors gracefully ensures users receive meaningful messages instead of stack traces.

2. Basic Exception Handling Using Try-Catch Blocks

At the simplest level, you can use try-catch blocks to handle exceptions within methods.

@RestController
@RequestMapping("/api")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/user/{id}")
    public ResponseEntity<?> getUserById(@PathVariable Long id) {
        try {
            User user = userService.getUserById(id);
            return ResponseEntity.ok(user);
        } catch (UserNotFoundException ex) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body("User not found: " + ex.getMessage());
        } catch (Exception ex) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An unexpected error occurred.");
        }
    }
}

This approach works for isolated cases but leads to code duplication and poor maintainability across controllers.

3. Using @ExceptionHandler for Controller-Specific Handling

Spring Boot provides the @ExceptionHandler annotation to handle exceptions within a specific controller.

@RestController
@RequestMapping("/api")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/user/{id}")
    public ResponseEntity<User> getUserById(@PathVariable Long id) {
        User user = userService.getUserById(id);
        return ResponseEntity.ok(user);
    }

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<String> handleUserNotFoundException(UserNotFoundException ex) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body("User not found: " + ex.getMessage());
    }
}

Pros:

  • Keeps exception-handling logic close to the controller.

  • Avoids redundant try-catch blocks.

Cons:

  • If multiple controllers need similar exception handling, this results in duplicated @ExceptionHandler methods.

4. Global Exception Handling Using @ControllerAdvice

A cleaner and more scalable approach is to use @ControllerAdvice to handle exceptions globally.

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<String> handleUserNotFoundException(UserNotFoundException ex) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body("User not found: " + ex.getMessage());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<String> handleValidationException(MethodArgumentNotValidException ex) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Validation failed: " + ex.getMessage());
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleGenericException(Exception ex) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An unexpected error occurred.");
    }
}

Benefits:

  • Centralized exception handling for multiple controllers.

  • Cleaner and more maintainable code.

  • Better separation of concerns.

5. Customizing Error Responses with @ExceptionHandler

Instead of returning plain strings, we can create a structured error response.

public class ErrorResponse {
    private String message;
    private int statusCode;
    private LocalDateTime timestamp;

    public ErrorResponse(String message, int statusCode) {
        this.message = message;
        this.statusCode = statusCode;
        this.timestamp = LocalDateTime.now();
    }

    // Getters and setters
}

Using this custom response in GlobalExceptionHandler:

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleUserNotFoundException(UserNotFoundException ex) {
        ErrorResponse errorResponse = new ErrorResponse(ex.getMessage(), HttpStatus.NOT_FOUND.value());
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) {
        ErrorResponse errorResponse = new ErrorResponse("Internal Server Error", HttpStatus.INTERNAL_SERVER_ERROR.value());
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
    }
}

This makes the API responses more structured:

{
    "message": "User not found",
    "statusCode": 404,
    "timestamp": "2024-06-15T12:30:45"
}

6. Handling Validation Errors in Spring Boot

Spring Boot automatically validates request data using @Valid and @Validated annotations.

Example: Validating a DTO

public class UserDTO {

    @NotNull(message = "Name is required")
    @Size(min = 3, message = "Name must be at least 3 characters long")
    private String name;

    @Email(message = "Invalid email format")
    private String email;

    // Getters and setters
}

Applying Validation in a Controller

@PostMapping("/user")
public ResponseEntity<String> createUser(@Valid @RequestBody UserDTO userDTO) {
    return ResponseEntity.ok("User created successfully!");
}

Handling Validation Exceptions

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationException(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);
}

This results in structured validation error responses:

{
    "name": "Name must be at least 3 characters long",
    "email": "Invalid email format"
}

7. Using ResponseStatusException for Custom Exceptions

Spring Boot provides ResponseStatusException for throwing exceptions with HTTP status codes.

public User getUserById(Long id) {
    return userRepository.findById(id)
        .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "User not found"));
}

This automatically returns:

{
    "status": 404,
    "error": "Not Found",
    "message": "User not found"
}

Conclusion

Spring Boot offers multiple ways to handle exceptions, from basic try-catch blocks to global exception handling with @ControllerAdvice. Using structured error responses and validation error handling enhances API usability.

Key Takeaways:

  • Use @ExceptionHandler for controller-specific exceptions.

  • Leverage @ControllerAdvice for centralized error handling.

  • Return structured error responses instead of plain strings.

  • Use validation annotations and handle validation errors properly.

  • Consider ResponseStatusException for concise exception handling.

By implementing these best practices, you can build more reliable and user-friendly Spring Boot applications.

0
Subscribe to my newsletter

Read articles from Muhire Josué directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Muhire Josué
Muhire Josué

I am a backend developer, interested in writing about backend engineering, DevOps and tooling.