DEV Community

keping jiang
keping jiang

Posted on

How I Handle Global Exception Handling in a Spring Boot Application

How I Handle Global Exception Handling in a Spring Boot Application

Exception handling is one of those things that often starts simple and gradually becomes messy.

At the beginning of a Spring Boot application, it is common to see code like this:

try {
    userService.createUser(request);
} catch (Exception e) {
    return ResponseEntity
            .status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body("Something went wrong");
}
Enter fullscreen mode Exit fullscreen mode

This might work for a small application.

But as the application grows, repeating exception handling logic across controllers quickly becomes difficult to maintain.

In this article, I want to share a simple approach I use to structure global exception handling in a Spring Boot application.

The goals are straightforward:

  • Keep controllers clean
  • Return consistent API responses
  • Separate business errors from system errors
  • Make the application easier to maintain

The Problem with Handling Exceptions in Every Controller

Imagine an application with many endpoints.

Without centralized exception handling, you might end up with something like this:

@PostMapping("/users")
public ResponseEntity<?> createUser(
        @RequestBody CreateUserRequest request) {

    try {

        User user = userService.createUser(request);

        return ResponseEntity.ok(user);

    } catch (UserAlreadyExistsException e) {

        return ResponseEntity
                .status(HttpStatus.CONFLICT)
                .body(e.getMessage());

    } catch (Exception e) {

        return ResponseEntity
                .status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body("Internal server error");
    }
}
Enter fullscreen mode Exit fullscreen mode

Then another controller might contain similar code.

And another.

Over time, this creates several problems:

  • Repeated code
  • Inconsistent error responses
  • Controllers become harder to read
  • Exception logic becomes scattered across the application

I prefer to move this responsibility into one centralized place.


1. Start with Custom Exceptions

The first step is to distinguish between different types of errors.

For example:

public class ResourceNotFoundException
        extends RuntimeException {

    public ResourceNotFoundException(String message) {
        super(message);
    }
}
Enter fullscreen mode Exit fullscreen mode

Another example:

public class BusinessException
        extends RuntimeException {

    public BusinessException(String message) {
        super(message);
    }
}
Enter fullscreen mode Exit fullscreen mode

For duplicate resources:

public class ResourceAlreadyExistsException
        extends RuntimeException {

    public ResourceAlreadyExistsException(String message) {
        super(message);
    }
}
Enter fullscreen mode Exit fullscreen mode

These exceptions make the business logic easier to understand.

For example:

public User findById(Long id) {

    return userRepository
            .findById(id)
            .orElseThrow(() ->
                    new ResourceNotFoundException(
                            "User not found"
                    )
            );
}
Enter fullscreen mode Exit fullscreen mode

The service does not need to know anything about HTTP responses.

It simply expresses what went wrong.


2. Create a Consistent Error Response

One thing I strongly recommend is using a consistent error response format.

For example:

{
  "timestamp": "2026-08-15T10:30:00",
  "status": 404,
  "error": "NOT_FOUND",
  "message": "User not found",
  "path": "/api/users/123"
}
Enter fullscreen mode Exit fullscreen mode

We can represent this with a simple class:

public class ErrorResponse {

    private LocalDateTime timestamp;

    private int status;

    private String error;

    private String message;

    private String path;

    // constructors
    // getters
}
Enter fullscreen mode Exit fullscreen mode

Using one standard response format makes the API easier to consume.

Clients do not need to handle a completely different error structure for every endpoint.


3. Use @RestControllerAdvice

Spring provides a very useful mechanism for centralized exception handling:

@RestControllerAdvice
public class GlobalExceptionHandler {
}
Enter fullscreen mode Exit fullscreen mode

Now we can define handlers for different exceptions.

For example:

@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(
        ResourceNotFoundException ex,
        HttpServletRequest request) {

    ErrorResponse response = new ErrorResponse(
            LocalDateTime.now(),
            HttpStatus.NOT_FOUND.value(),
            HttpStatus.NOT_FOUND.getReasonPhrase(),
            ex.getMessage(),
            request.getRequestURI()
    );

    return ResponseEntity
            .status(HttpStatus.NOT_FOUND)
            .body(response);
}
Enter fullscreen mode Exit fullscreen mode

Now whenever a ResourceNotFoundException is thrown anywhere in the application, Spring can handle it automatically.

The controller stays clean.


4. Handle Business Exceptions Separately

Not every exception is a system failure.

Sometimes an error is part of normal business behavior.

For example:

User already exists
Enter fullscreen mode Exit fullscreen mode

Or:

Account is inactive
Enter fullscreen mode Exit fullscreen mode

Or:

Document processing is not allowed
Enter fullscreen mode Exit fullscreen mode

These can be handled separately.

@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusinessException(
        BusinessException ex,
        HttpServletRequest request) {

    ErrorResponse response = new ErrorResponse(
            LocalDateTime.now(),
            HttpStatus.BAD_REQUEST.value(),
            HttpStatus.BAD_REQUEST.getReasonPhrase(),
            ex.getMessage(),
            request.getRequestURI()
    );

    return ResponseEntity
            .badRequest()
            .body(response);
}
Enter fullscreen mode Exit fullscreen mode

This gives the client a predictable response.


5. Handle Validation Errors

Validation errors are another important case.

For example:

public class CreateUserRequest {

    @NotBlank
    private String username;

    @Email
    private String email;

    @NotBlank
    private String password;
}
Enter fullscreen mode Exit fullscreen mode

When validation fails, Spring throws a validation exception.

We can handle it globally:

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationException(
        MethodArgumentNotValidException ex,
        HttpServletRequest request) {

    String message = ex
            .getBindingResult()
            .getFieldErrors()
            .stream()
            .map(error ->
                    error.getField()
                    + ": "
                    + error.getDefaultMessage()
            )
            .findFirst()
            .orElse("Validation failed");

    ErrorResponse response = new ErrorResponse(
            LocalDateTime.now(),
            HttpStatus.BAD_REQUEST.value(),
            HttpStatus.BAD_REQUEST.getReasonPhrase(),
            message,
            request.getRequestURI()
    );

    return ResponseEntity
            .badRequest()
            .body(response);
}
Enter fullscreen mode Exit fullscreen mode

For more complex APIs, you may want to return all validation errors instead of just the first one.

For example:

{
  "timestamp": "2026-08-15T10:30:00",
  "status": 400,
  "error": "BAD_REQUEST",
  "message": "Validation failed",
  "errors": {
    "username": "must not be blank",
    "email": "must be a valid email address"
  }
}
Enter fullscreen mode Exit fullscreen mode

This can provide a better experience for frontend developers and API consumers.


6. Always Have a Fallback Exception Handler

Finally, I recommend having a handler for unexpected exceptions.

@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(
        Exception ex,
        HttpServletRequest request) {

    ErrorResponse response = new ErrorResponse(
            LocalDateTime.now(),
            HttpStatus.INTERNAL_SERVER_ERROR.value(),
            HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(),
            "An unexpected error occurred",
            request.getRequestURI()
    );

    return ResponseEntity
            .status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(response);
}
Enter fullscreen mode Exit fullscreen mode

However, there is an important detail here.

I generally do not return the full exception message to the client.

For example, this is usually not a good idea:

.body(ex.getMessage());
Enter fullscreen mode Exit fullscreen mode

Unexpected exceptions may contain implementation details or sensitive information.

Instead:

Client receives:
An unexpected error occurred
Enter fullscreen mode Exit fullscreen mode

While the actual exception should be logged internally.

For example:

log.error("Unexpected error", ex);
Enter fullscreen mode Exit fullscreen mode

This keeps internal implementation details away from API clients while still making debugging possible.


7. Keep Controllers Focused on HTTP Requests

With global exception handling in place, controllers become much simpler.

Instead of this:

@PostMapping("/users")
public ResponseEntity<?> createUser(
        @RequestBody CreateUserRequest request) {

    try {

        User user = userService.createUser(request);

        return ResponseEntity.ok(user);

    } catch (Exception e) {

        return ResponseEntity
                .status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body("Something went wrong");
    }
}
Enter fullscreen mode Exit fullscreen mode

We can write:

@PostMapping("/users")
public ResponseEntity<User> createUser(
        @Valid @RequestBody CreateUserRequest request) {

    User user = userService.createUser(request);

    return ResponseEntity.ok(user);
}
Enter fullscreen mode Exit fullscreen mode

The controller focuses on:

HTTP Request
        ↓
Validation
        ↓
Service
        ↓
HTTP Response
Enter fullscreen mode Exit fullscreen mode

Exception handling happens separately.

This makes the application easier to read and maintain.


8. My Basic Exception Structure

For a typical Spring Boot application, I like to keep the structure simple:

exception
├── BusinessException
├── ResourceNotFoundException
├── ResourceAlreadyExistsException
└── GlobalExceptionHandler
Enter fullscreen mode Exit fullscreen mode

As the application grows, additional exception types can be added when they represent meaningful business concepts.

The important thing is not to create dozens of exception classes unnecessarily.

Start with a small set:

Not Found
Conflict
Business Error
Validation Error
Unexpected Error
Enter fullscreen mode Exit fullscreen mode

That is often enough for many applications.


A Simple Mental Model

I think about exceptions in three categories:

Business Errors
        ↓
Expected application behavior

Validation Errors
        ↓
Invalid client input

System Errors
        ↓
Unexpected failures
Enter fullscreen mode Exit fullscreen mode

Each category should have a predictable API response.

For example:

Error Type HTTP Status
Resource not found 404
Invalid request 400
Resource already exists 409
Unauthorized 401
Forbidden 403
Unexpected error 500

The exact structure may vary depending on the application, but consistency is important.


Final Thoughts

Global exception handling is not one of the most exciting parts of a Spring Boot application.

But it has a big impact on the overall quality of the API.

A good exception handling strategy should:

  • Keep controllers clean
  • Separate business logic from HTTP concerns
  • Return consistent error responses
  • Avoid exposing internal implementation details
  • Make the application easier to maintain

My preferred starting point is simple:

Custom Exceptions
        ↓
@RestControllerAdvice
        ↓
Consistent Error Response
        ↓
Structured Logging
Enter fullscreen mode Exit fullscreen mode

You do not need a complicated exception framework to get started.

A small and consistent structure is usually enough.

As the application grows, you can evolve the error model based on real requirements.


How do you handle exceptions in your Spring Boot applications?

Do you use:

  • @RestControllerAdvice?
  • Custom business exceptions?
  • A standard API error response?
  • Problem Details / RFC 9457?

I'd love to hear how other Java developers structure exception handling in production applications.


I'm building and sharing open-source projects around Java, Spring Boot, enterprise application architecture, and AI-powered applications.

My goal is to explore practical patterns and reusable tools that help developers build enterprise applications faster.

Top comments (0)