DEV Community

realNameHidden
realNameHidden

Posted on

How Do You Map Different Exceptions to Different HTTP Status Codes in Spring Boot?

Learn how to map different exceptions to HTTP status codes in Spring Boot using @ControllerAdvice and @ExceptionHandler. Beginner-friendly, end-to-end Java 21 example.

Introduction

Imagine you order food online and the app simply says “Something went wrong”—no hint whether the restaurant is closed, the item is unavailable, or payment failed. Frustrating, right?

The same frustration happens when APIs return wrong or generic HTTP status codes.

In Spring Boot, throwing exceptions is easy—but mapping them correctly to meaningful HTTP status codes is what separates a beginner API from a professional one. A 404 should mean not found, a 400 should mean bad input, and a 500 should mean server failure.

In this blog, you’ll learn how to map different exceptions to different HTTP status codes in Spring Boot, using clear explanations, real-world analogies, and an end-to-end Java 21 example.


Core Concepts

What Does “Mapping Exceptions” Mean?

Think of exceptions as signals and HTTP status codes as traffic lights 🚦.

Situation Exception HTTP Status
Resource not found UserNotFoundException 404 NOT_FOUND
Invalid input IllegalArgumentException 400 BAD_REQUEST
Business rule violation BusinessException 409 CONFLICT
Unexpected failure Exception 500 INTERNAL_SERVER_ERROR

Mapping exceptions means:

Telling Spring Boot which HTTP status code to return when a specific exception occurs


Why Is This Important?

Proper exception-to-status mapping gives you:

✅ Clear communication with API consumers

✅ Easier debugging

✅ REST-compliant APIs

✅ Cleaner controller code

And the best part?

👉 You do this centrally, not in every controller.


The Key Tools

  1. Custom Exceptions – Represent business problems
  2. @ControllerAdvice – Global exception interceptor
  3. @ExceptionHandler – Maps exception → HTTP response
  4. ResponseEntity – Full control over status & body

Code Examples (End-to-End)

Let’s build a complete working example step by step.


🧱 Example 1: Mapping Custom Exceptions to HTTP Status Codes

Step 1: Create Custom Exceptions

package com.example.demo.exception;

/**
 * Thrown when a user is not found in the system.
 */
public class UserNotFoundException extends RuntimeException {

    public UserNotFoundException(String message) {
        super(message);
    }
}
Enter fullscreen mode Exit fullscreen mode
package com.example.demo.exception;

/**
 * Thrown when a business rule is violated.
 */
public class BusinessException extends RuntimeException {

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

Step 2: Service Layer (Where Exceptions Are Thrown)

package com.example.demo.service;

import com.example.demo.exception.BusinessException;
import com.example.demo.exception.UserNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    public String getUserById(Long id) {

        if (id <= 0) {
            throw new IllegalArgumentException("User ID must be greater than zero");
        }

        if (id == 404) {
            throw new UserNotFoundException("User not found with id " + id);
        }

        if (id == 409) {
            throw new BusinessException("User account is locked");
        }

        return "User fetched successfully";
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: REST Controller (Clean & Simple)

package com.example.demo.controller;

import com.example.demo.service.UserService;
import org.springframework.web.bind.annotation.*;

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

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/{id}")
    public String getUser(@PathVariable Long id) {
        return userService.getUserById(id);
    }
}
Enter fullscreen mode Exit fullscreen mode

📌 Notice:
No try-catch blocks in the controller ✔
This is the power of global exception handling.


🧠 Example 2: Global Exception Mapping with @ControllerAdvice

Step 4: Global Exception Handler

package com.example.demo.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

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

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<Map<String, Object>> handleUserNotFound(
            UserNotFoundException ex) {

        return ResponseEntity.status(HttpStatus.NOT_FOUND)
                .body(buildErrorResponse(ex.getMessage(), HttpStatus.NOT_FOUND));
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<Map<String, Object>> handleBadRequest(
            IllegalArgumentException ex) {

        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(buildErrorResponse(ex.getMessage(), HttpStatus.BAD_REQUEST));
    }

    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<Map<String, Object>> handleBusinessException(
            BusinessException ex) {

        return ResponseEntity.status(HttpStatus.CONFLICT)
                .body(buildErrorResponse(ex.getMessage(), HttpStatus.CONFLICT));
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<Map<String, Object>> handleGenericException(
            Exception ex) {

        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(buildErrorResponse(
                        "Unexpected error occurred",
                        HttpStatus.INTERNAL_SERVER_ERROR));
    }

    private Map<String, Object> buildErrorResponse(String message, HttpStatus status) {
        return Map.of(
                "timestamp", LocalDateTime.now(),
                "status", status.value(),
                "error", status.getReasonPhrase(),
                "message", message
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

✅ Final API Behavior

Request Result
http://localhost:8080/users/0 400 BAD_REQUEST
http://localhost:8080/users/404 404 NOT_FOUND
http://localhost:8080/users/409 409 CONFLICT
http://localhost:8080/users/1 200 OK
http://localhost:8080/ 500 INTERNAL_SERVER_ERROR

✔ Clean
✔ Predictable
✔ REST-compliant

This is mapping exceptions to HTTP status codes in Spring Boot done right.


Best Practices

  1. Create meaningful custom exceptions
    Avoid throwing generic RuntimeException everywhere.

  2. Handle exceptions globally
    Never clutter controllers with try-catch blocks.

  3. Use correct HTTP status codes
    400 ≠ 404 ≠ 409 ≠ 500 — each has a purpose.

  4. Never expose internal stack traces
    Log them internally, return safe messages to clients.

  5. Use a consistent error response format
    Makes life easier for frontend & API consumers.


Conclusion

Mapping exceptions correctly is a core skill for any Spring Boot developer.

By combining:

  • Custom exceptions
  • @ControllerAdvice
  • @ExceptionHandler
  • Proper HTTP status codes

you can build clean, professional, and consumer-friendly APIs.

If you’re serious about Java programming and want to learn Java with real-world practices, mastering mapping exceptions to HTTP status codes in Spring Boot is non-negotiable.


Call to Action

💬 Have questions about exception handling or REST API design?
👇 Drop them in the comments!

Want advanced topics like:

  • RFC 7807 (Problem Details)
  • API Gateway–friendly error models
  • Exception handling with Apigee

Just ask — happy to help 🚀


🔗 Helpful Resources



Enter fullscreen mode Exit fullscreen mode

Top comments (0)