☕ Java Q110 / 162

How do you handle errors in a Spring Boot application?

AI-Powered Answer ✓ Answered

Error handling is a critical aspect of building robust Spring Boot applications. Proper error management ensures that your application responds gracefully to unexpected situations, provides meaningful feedback to clients, and maintains a stable user experience. Spring Boot offers several powerful mechanisms to centralize and customize error handling.

Spring Boot's Default Error Handling

By default, Spring Boot provides a basic error page (either a 'Whitelabel Error Page' in the browser or a JSON error object for REST clients) when an uncaught exception occurs or a servlet error happens (e.g., a 404 Not Found or 500 Internal Server Error). While convenient for development, this default page is not suitable for production environments. For REST APIs, the default includes a timestamp, status, error, path, and message.

Centralized Error Handling with @ControllerAdvice and @ExceptionHandler

The @ControllerAdvice annotation allows you to define a global error handler for your application. When combined with @ExceptionHandler, it provides a powerful way to intercept exceptions thrown across all @Controller and @RestController components and handle them in a centralized manner.

Using @ControllerAdvice and @ExceptionHandler

Create a class annotated with @ControllerAdvice and define methods annotated with @ExceptionHandler that specify the type of exception they should handle. These methods can return ResponseEntity for REST APIs or a ModelAndView for web applications.

java
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.Map;

@ControllerAdvice
public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(MyCustomNotFoundException.class)
    public ResponseEntity<Object> handleCityNotFoundException(
            MyCustomNotFoundException ex, WebRequest request) {

        Map<String, Object> body = new LinkedHashMap<>();
        body.put("timestamp", LocalDateTime.now());
        body.put("message", "Item not found: " + ex.getMessage());

        return new ResponseEntity<>(body, HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<Object> handleIllegalArgumentException(
            IllegalArgumentException ex, WebRequest request) {

        Map<String, Object> body = new LinkedHashMap<>();
        body.put("timestamp", LocalDateTime.now());
        body.put("message", "Invalid argument provided: " + ex.getMessage());

        return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST);
    }

    // You can also handle generic exceptions
    @ExceptionHandler(Exception.class)
    public ResponseEntity<Object> handleAllUncaughtException(
            Exception ex, WebRequest request) {

        Map<String, Object> body = new LinkedHashMap<>();
        body.put("timestamp", LocalDateTime.now());
        body.put("message", "An unexpected error occurred: " + ex.getMessage());

        return new ResponseEntity<>(body, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

In the example above, MyCustomNotFoundException and IllegalArgumentException are handled specifically, returning a custom JSON response with appropriate HTTP status codes. A generic Exception.class handler catches any other unhandled exceptions, preventing the default Spring Boot error page from appearing.

Custom Error Pages for Web Applications

For traditional server-side rendered applications, you might want to display custom HTML error pages. Spring Boot looks for error pages in /resources/static/error or /resources/templates/error with names matching the HTTP status code (e.g., 404.html, 500.html). You can also create an error.html to act as a fallback for any status code without a specific page.

properties
# application.properties
server.error.whitelabel.enabled=false
server.error.path=/error

Implementing a Custom ErrorController

For more fine-grained control over the default /error endpoint provided by Spring Boot, you can implement your own ErrorController. This allows you to customize the content and logic for various error scenarios.

java
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.HashMap;
import java.util.Map;

@Controller
public class CustomErrorController implements ErrorController {

    private static final String PATH = "/error";

    @RequestMapping(value = PATH)
    @ResponseBody
    public ResponseEntity<Map<String, Object>> handleError(HttpServletRequest request) {
        HttpStatus status = getStatus(request);
        String message = (String) request.getAttribute(RequestDispatcher.ERROR_MESSAGE);
        Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
        Exception exception = (Exception) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);

        Map<String, Object> errorDetails = new HashMap<>();
        errorDetails.put("status", status.value());
        errorDetails.put("error", status.getReasonPhrase());
        errorDetails.put("message", message != null ? message : "No specific message available");
        errorDetails.put("timestamp", System.currentTimeMillis());

        if (exception != null) {
            errorDetails.put("exception", exception.getClass().getName());
            errorDetails.put("exceptionMessage", exception.getMessage());
        }

        return new ResponseEntity<>(errorDetails, status);
    }

    private HttpStatus getStatus(HttpServletRequest request) {
        Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
        if (statusCode == null) {
            return HttpStatus.INTERNAL_SERVER_ERROR;
        }
        try {
            return HttpStatus.valueOf(statusCode);
        } catch (IllegalArgumentException ex) {
            return HttpStatus.INTERNAL_SERVER_ERROR;
        }
    }

    @Override
    public String getErrorPath() {
        return PATH;
    }
}

This CustomErrorController overrides Spring Boot's default error handling by directly handling requests to the /error path. It extracts error information from the HttpServletRequest and formats a custom JSON response.

Custom Exception Classes

Creating custom exception classes for domain-specific errors improves code readability and allows for more granular error handling with @ExceptionHandler. You can make them checked or unchecked, though unchecked (extending RuntimeException) are more common in Spring applications to avoid verbose throws clauses.

java
public class MyCustomNotFoundException extends RuntimeException {
    public MyCustomNotFoundException(String message) {
        super(message);
    }
}

// In a service or controller
throw new MyCustomNotFoundException("User with ID " + userId + " not found.");

Error Attributes Customization

Spring Boot provides an ErrorAttributes interface (and DefaultErrorAttributes implementation) to control the attributes included in the error response. You can implement a custom ErrorAttributes bean to add, remove, or modify default attributes like timestamp, status, error, message, path, etc. This is particularly useful for exposing only necessary information to clients.

Best Practices for Error Handling

  • Log Errors: Always log exceptions with sufficient detail (stack trace, relevant context) using a robust logging framework like SLF4J/Logback.
  • Be Specific: Handle specific exceptions rather than relying solely on generic Exception.class handlers. This allows for more precise responses.
  • Consistent API Responses: For REST APIs, maintain a consistent JSON structure for error responses to make it easier for client applications to parse and react to errors.
  • Meaningful Messages: Provide clear, user-friendly error messages that help clients understand what went wrong, but avoid exposing sensitive internal details.
  • Appropriate HTTP Status Codes: Always return the correct HTTP status code (e.g., 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 500 Internal Server Error) relevant to the error.
  • Avoid Catching and Swallowing: Don't catch exceptions and do nothing with them. Either handle them, rethrow them, or wrap them in a more specific exception.
  • Consider Problem Details (RFC 7807): For advanced REST APIs, consider returning error responses that conform to the Problem Details for HTTP APIs standard to provide richer error information.