Implementing Robust Global Exception Handling in Spring Boot

Implementing Robust Global Exception Handling in Spring Boot

How to Handle Errors in Spring Boot: An Architectural Guide

While working on a Spring Framework project, handling exceptions is crucial. When something goes wrong in the code, Java throws an exception. If we don't handle it properly, the stack trace text will be shown to the user. This might be unprofessional and might leak security to the users.

How Error Handling Works

Instead of handling exceptions inside every method of the code, Spring Boot provides a mechanism to build global exception handling.
Let's assume the security guard is standing at the exit of our application. No matter where the exception or error happens in any part of the code, it will throw to the guard, who then turns it into a readable message before sending it to the users.

Core Architecture

Article Image

Error Message Formatting

When the errors occur in our application, we need to format them and send a readable message to the users.
Let's create the utility class for this.
java
package com.csbyte.exceptions;


import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.Instant;
import java.util.HashMap;
import java.util.Map;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Error {

    private static final long serialVersionUID = 1L;
    /**
     * HTTP status code set by the origin server.
     */
    private Integer code;

    /**
     * Error message.
     */
    private String message;

    /**
     * Url of request that produced the error.
     */
    private String url = "Not available";

    /**
     * Method of request that produced the error.
     */
    private String reqMethod = "Not available";

    /**
     * Timestamp
     */
    private Instant timestamp;

    Map<String, String> errors = new HashMap<>();
}
We are using Lambok to get rid of setter and getter methods boilerplate code. We can use the Java record as well. This class defines the simple response for errors.

Creating a Helper Utility

To create code clean and reduce the boilerplate code, we will create a utils class that builds the error response.
java
package com.csbyte.exceptions;

import jakarta.servlet.http.HttpServletRequest;

import java.time.Instant;

public class ErrorUtils {

  public static Error createError(HttpServletRequest request, String message, Integer code) {
    return Error.builder()
            .code(code)
            .message(message)
            .reqMethod(request.getMethod())
            .url(request.getRequestURL().toString())
            .timestamp(Instant.now())
            .build();
  }

}

Creating Custom Errors

Instead of throwing a generic RuntimeException exception, it's best practice to create some custom exceptions that might be used in the code to throw application custom errors.
java
package com.csbyte.exceptions;

public class DataNotFoundException extends RuntimeException {

    private static final long serialVersionUID = 1L;

    String field;
    String fieldName;
    Long fieldId;

    public DataNotFoundException(String fieldName, String field, Long fieldId) {
        super(String.format("%s not found with %s: %d", fieldName, field, fieldId));
        this.fieldName = fieldName;
        this.field = field;
        this.fieldId = fieldId;
    }
}
If the app tries to query the items in the database that don't exist, we can use this exception to capture exactly what went missing.
java
package com.csbyte.exceptions;

import java.io.Serial;

public class APIException extends RuntimeException {

    @Serial
    private static final long serialVersionUID = 1L;

    public APIException() {
    }

    public APIException(String message) {
        super(message);
    }
}
This is the generic simple bad request api exception. If a user tries to do something invalid, we can throw this exception.
You can create the desired exception in accordance with the application requirement.

Creating Global Exception Handling

Spring provides a clean way to handle exceptions globally using ControllerAdvice annotations. We are using RestControllerAdvice a user for rest api services.
Let's create a class that will handle multiple exceptions.
java
package com.csbyte.exceptions;

import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

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


@RestControllerAdvice
public class GlobalExceptionHandler {

	@ExceptionHandler(APIException.class)
	public ResponseEntity<Error> apiException(HttpServletRequest request, APIException e) {
		String message = e.getMessage();
		Error error = ErrorUtils.createError(request, message, HttpStatus.BAD_REQUEST.value());
		return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
	}


	@ExceptionHandler(DataNotFoundException.class)
	public ResponseEntity<Error> dataNotFoundException(HttpServletRequest request, DataNotFoundException e) {
		String message = e.getMessage();
		Error error = ErrorUtils.createError(request, message, HttpStatus.NOT_FOUND.value());
		return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
	}


	@ExceptionHandler(MethodArgumentNotValidException.class)
	public ResponseEntity<Error> handleValidationExceptions(HttpServletRequest request, MethodArgumentNotValidException e) {
		Map<String, String> errors = new HashMap<>();
		e.getBindingResult().getAllErrors().forEach((error) -> {
			String fieldName = ((FieldError) error).getField();
			String errorMessage = error.getDefaultMessage();
			errors.put(fieldName, errorMessage);
		});
		Error error = ErrorUtils.createError(request, "Field Validation failed", HttpStatus.BAD_REQUEST.value());
		error.errors = errors;
		return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
	}
}
The GlobalExceptionHandler class is responsible for handling various exceptions that may occur during API requests and providing appropriate responses. It uses Spring's exception handling mechanisms to customize error responses for different types of exceptions.
We are using the ErrorUtils class to format the error message that was created previously. Let's look into the one exception handling.
java
	@ExceptionHandler(MethodArgumentNotValidException.class)
	public ResponseEntity<Error> handleValidationExceptions(HttpServletRequest request, MethodArgumentNotValidException e) {
		Map<String, String> errors = new HashMap<>();
		e.getBindingResult().getAllErrors().forEach((error) -> {
			String fieldName = ((FieldError) error).getField();
			String errorMessage = error.getDefaultMessage();
			errors.put(fieldName, errorMessage);
		});
		Error error = ErrorUtils.createError(request, "Field Validation failed", HttpStatus.BAD_REQUEST.value());
		error.errors = errors;
		return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
	}
Here, when the incoming request fails due to validation, such as (e.g, @NotNull, @Size), Spring throws MethodArgumentNotValidException an exception which is then caught by this handler. This handler intercepts it, loops through the errors, extracts the faulty property field alongside its default constraint message, and injects the map cleanly into the Error object response payload.

Sample Implementation

Let's look at the sample example that throws a custom exception if the article data is not found in the database.
java
    public ArticleResponse updateArticle(Long id, ArticleRequest request) {

        ArticleEntity article = articleRepository.findById(id)
                .orElseThrow(() -> new DataNotFoundException("Article not found", "id",           id));
        ----------------
    }
In this way, by combining Spring Boot @RestControllerAdvice with a custom exception, we can easily handle the exception in the application.