Java  

Handle Exceptions Globally in a Spring Boot Application?

Introduction

When building a Spring Boot application, errors and exceptions are inevitable—such as invalid data, missing records, or server failures. If each controller handles errors separately, the code becomes messy and complicated to maintain. A better approach is Global Exception Handling. Spring Boot provides powerful tools such as @ControllerAdvice and @ExceptionHandler to handle exceptions cleanly and consistently. In this article, we explain how to implement global exception handling in plain language.

Why Global Exception Handling Is Important

A good error-handling system:

  • Keeps the code clean

  • Returns consistent error messages

  • Improves debugging

  • Enhances user experience

  • Helps maintain clean APIs for frontend or mobile apps

Instead of writing try-catch blocks everywhere, you can manage all exceptions in one place.

Step 1: Create Custom Exception Classes

Custom exceptions make it easy to identify specific error scenarios.

Example: Exception when user is not found

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

Example: Exception for invalid input

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

Step 2: Create a Global Exception Handler Using @ControllerAdvice

@ControllerAdvice tells Spring Boot to look here for handling all exceptions.

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<String> handleUserNotFound(UserNotFoundException ex) {
        return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(InvalidInputException.class)
    public ResponseEntity<String> handleInvalidInput(InvalidInputException ex) {
        return new ResponseEntity<>(ex.getMessage(), HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleGeneralErrors(Exception ex) {
        return new ResponseEntity<>("Something went wrong!", HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

This ensures every exception gets a proper and consistent response.

Step 3: Create a Standard Error Response Structure

Instead of returning plain text, send a clear JSON structure.

public class ErrorResponse {
    private String message;
    private LocalDateTime timestamp;
    private String path;

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

    // getters & setters
}

Update your exception handler to return JSON:

@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<ErrorResponse> handleUserNotFound(UserNotFoundException ex, HttpServletRequest request) {
    ErrorResponse response = new ErrorResponse(ex.getMessage(), request.getRequestURI());
    return new ResponseEntity<>(response, HttpStatus.NOT_FOUND);
}

Step 4: Throw Exceptions from Controller or Service

Controller:

@GetMapping("/users/{id}")
public User getUser(@PathVariable int id) {
    return userService.getUserById(id);
}

Service class:

public User getUserById(int id) {
    return userRepository.findById(id)
            .orElseThrow(() -> new UserNotFoundException("User not found with ID: " + id));
}

Now your global handler handles this error automatically.

Step 5: Handle Validation Errors

Use @Valid and handle MethodArgumentNotValidException.

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
    String errorMsg = ex.getBindingResult().getFieldError().getDefaultMessage();
    ErrorResponse response = new ErrorResponse(errorMsg, "Validation Error");
    return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
}

This ensures clean validation error responses.

Step 6: Handle Global Errors Using @RestControllerAdvice

@RestControllerAdvice returns JSON automatically.

@RestControllerAdvice
public class ApiExceptionHandler {

    @ExceptionHandler(Exception.class)
    public ErrorResponse handleAll(Exception ex) {
        return new ErrorResponse("Internal Server Error", "Unknown");
    }
}

Real-Life Example

A travel booking company in India uses a Spring Boot backend. When a user tries to book a ticket with an invalid ID, the API returns a clean JSON message such as:

{
  "message": "Booking not found",
  "timestamp": "2025-01-12T10:20:30",
  "path": "/bookings/99"
}

This helps frontend developers show readable error messages to the users.

Best Practices

  • Keep exception messages meaningful

  • Use custom exceptions for clarity

  • Never expose internal server errors to users

  • Log exceptions properly

  • Use a unified structure for all error responses

Comparison Table: Local vs Global Exception Handling

FeatureLocal Exception HandlingGlobal Exception Handling
ScopeHandles exceptions inside a single controller or methodHandles exceptions across the whole application (all controllers)
Code LocationTry-catch blocks inside controllers/servicesCentralized in @ControllerAdvice or @RestControllerAdvice
ReusabilityLow — duplicated handling in many placesHigh — one handler reused by all controllers
ConsistencyError responses may vary across endpointsProduces consistent error responses and status codes
MaintenanceHarder to maintain as app growsEasier to manage and update in one place
Use CaseQuick fixes, very specific scenariosRecommended for production APIs and large apps

Logging Integration with SLF4J

Logging is essential to understand runtime errors and track issues in production. Spring Boot supports SLF4J with Logback out of the box. Use SLF4J in your exception handlers and services for structured, searchable logs.

Add dependency (if needed)

Most Spring Boot starters include logging. If you need explicit Logback or SLF4J bindings, add:

<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-api</artifactId>
  <version>1.7.36</version>
</dependency>

Use logger in GlobalExceptionHandler

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@ControllerAdvice
public class GlobalExceptionHandler {

    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleUserNotFound(UserNotFoundException ex, HttpServletRequest request) {
        ErrorResponse response = new ErrorResponse(ex.getMessage(), request.getRequestURI());
        logger.warn("UserNotFound: path={}, message={}", request.getRequestURI(), ex.getMessage());
        return new ResponseEntity<>(response, HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGeneralErrors(Exception ex, HttpServletRequest request) {
        ErrorResponse response = new ErrorResponse("Something went wrong!", request.getRequestURI());
        logger.error("Unhandled exception at path={}", request.getRequestURI(), ex);
        return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

Log Levels and Best Practices

  • Use ERROR for unexpected failures and stack traces.

  • Use WARN for known but recoverable situations (e.g., resource not found).

  • Use INFO for significant application events (startup, shutdown, important state changes).

  • Avoid logging sensitive data (passwords, tokens, PII). Mask or omit them.

Example: Structured Logging (JSON) for Production

Configure a Logback encoder to output JSON (useful for log aggregators like ELK or Datadog). In logback-spring.xml you can add an encoder plugin or use logstash-logback-encoder to produce JSON logs that include timestamp, level, message, and exception.

Error Response Structure: Best Practices

A standard error response helps front-end apps and clients handle errors uniformly. Use a simple, predictable JSON structure that includes enough context but does not expose internal details.

Recommended Error Response Fields

  • timestamp — when the error occurred (ISO 8601).

  • status — HTTP status code (e.g., 404).

  • error — short error phrase (e.g., "Not Found").

  • message — human-readable message for the client.

  • path — the request path that caused the error.

  • requestId (optional) — unique ID for tracing logs across systems.

Example Error Response JSON

{
  "timestamp": "2025-12-09T16:30:00Z",
  "status": 404,
  "error": "Not Found",
  "message": "User not found with ID: 123",
  "path": "/api/users/123",
  "requestId": "req-9a8b7c6d"
}

How to Add requestId for Better Tracing

Generate or forward a requestId using a servlet filter or Spring OncePerRequestFilter. Put the requestId into MDC (Mapped Diagnostic Context) so logs automatically include it.

public class RequestIdFilter extends OncePerRequestFilter {
    private static final String REQUEST_ID = "requestId";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        String requestId = UUID.randomUUID().toString();
        MDC.put(REQUEST_ID, requestId);
        response.addHeader("X-Request-Id", requestId);
        try {
            filterChain.doFilter(request, response);
        } finally {
            MDC.remove(REQUEST_ID);
        }
    }
}

In your logger pattern or JSON encoder, include %X{requestId} to print the request id with every log line.

Summary

Global exception handling in Spring Boot provides a clean, consistent, and maintainable way to manage errors. Using @ControllerAdvice, custom exceptions, and structured JSON responses, developers can build professional backend systems that provide meaningful error messages and improve overall application quality.