Exception handling is essential for building robust, user-friendly APIs. With Spring Boot, you can centralize error handling using a Global Exception Handler and custom exceptions for clean, maintainable code. This guide shows you how to set up a modern global exception handler, create a custom exception, and use them in a controller—all in a way that's easy to understand and quick to implement.
1. Create a Custom Exception: ResourceNotFoundException
A custom exception helps you signal specific error cases (like a missing resource) in your business logic.
// filepath: src/main/java/com/example/exception/ResourceNotFoundException.java
package com.example.exception;
/**
* Thrown when a requested resource is not found.
*/
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
2. Implement a Global Exception Handler
Centralize your error handling for all controllers. This example uses a modern response format and logs errors for debugging.
// filepath: src/main/java/com/example/exception/GlobalExceptionHandler.java
package com.example.exception;
import com.example.utils.ApiResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.time.Instant;
import java.util.*;
import java.util.stream.Collectors;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
// Handle Resource Not Found exceptions
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ApiResponse<Object>> handleResourceNotFound(ResourceNotFoundException ex) {
ApiResponse<Object> response = ApiResponse.error(ex.getMessage(), "RESOURCE_NOT_FOUND");
return new ResponseEntity<>(response, HttpStatus.NOT_FOUND);
}
// Handle validation errors
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse<List<Map<String, String>>>> handleValidation(MethodArgumentNotValidException e) {
List<Map<String, String>> details = e.getBindingResult()
.getAllErrors()
.stream()
.map(error -> {
Map<String, String> errorDetail = new HashMap<>();
errorDetail.put("field", ((org.springframework.validation.FieldError) error).getField());
errorDetail.put("message", error.getDefaultMessage());
return errorDetail;
})
.collect(Collectors.toList());
ApiResponse.Meta meta = new ApiResponse.Meta(
Instant.now(),
UUID.randomUUID().toString(),
"1.0"
);
ApiResponse<List<Map<String, String>>> response = new ApiResponse<>(
"fail",
"Validation error!",
details,
meta,
null
);
return new ResponseEntity<>(response, HttpStatus.UNPROCESSABLE_ENTITY);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Object>> handleGenericException(Exception exception) {
log.error("Internal Server Error: {}", exception.getMessage(), exception);
ApiResponse<Object> response = ApiResponse.error(exception.getMessage(), "INTERNAL_SERVER_ERROR");
return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
3. Example Controller Using the Exception
Here’s a simple controller method that demonstrates how the custom exception and global handler work together.
Note: Service logic is placed in the controller for demonstration.
Best Practice: Move business logic to a service class for maintainability.
// filepath: src/main/java/com/example/controller/CategoryController.java
package com.example.controller;
import com.example.entity.Category;
import com.example.exception.ResourceNotFoundException;
import com.example.utils.ApiResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
@RestController
@RequestMapping("/api/v1/categories")
public class CategoryController {
// Example GET endpoint with inline logic for demonstration.
// Best Practice: Move this logic to a service class!
@GetMapping("/{id}")
public ResponseEntity<ApiResponse<Category>> getCategoryById(@PathVariable Long id) {
List<Category> categories = Arrays.asList(
new Category(1L, "Fruits", "Fresh fruits", true, "/images/fruits.png"),
new Category(2L, "Vegetables", "Green veggies", false, "/images/veggies.png")
);
Category found = categories.stream()
.filter(cat -> cat.getId().equals(id))
.findFirst()
.orElseThrow(() -> new ResourceNotFoundException("Category not found with id: " + id));
return ResponseEntity.ok(ApiResponse.success(found, "Category retrieved successfully"));
}
}
4. Sample Error Response
When a category is not found, the API returns:
{
"status": "fail",
"message": "Validation error!",
"data": [
{ "field": "name", "message": "Category name is required" }
],
"meta": {
"timestamp": "2025-09-17T12:34:56Z",
"requestId": "b1a2c3d4-e5f6-7890-abcd-ef1234567890",
"version": "1.0"
}
}
5. Best Practices
-
Centralize exception handling with
@RestControllerAdvice
. - Use custom exceptions for clear, domain-specific errors.
- Keep business logic in service classes for clean controllers.
- Return structured error responses for easy client integration.
- Log errors for debugging and monitoring.
Conclusion
With a global exception handler and custom exceptions, your Spring Boot API will be easier to maintain, debug, and consume.
Start with the patterns above and adapt them to your project for a modern, professional backend!
If you found this guide helpful, share it with your team or star your repository!
Top comments (0)