Label:
Spring Boot,Production Ready,Exception Handling,REST API,Clean Code
๐ Introduction
Robust exception handling is the cornerstone of any production-grade REST API. In Spring Boot, we aim to:
- Avoid duplicated try-catch blocks
- Standardize error responses
- Handle custom and system exceptions cleanly
- Keep logs clean for diagnostics
This blog will walk you through setting up a clean and reusable Global Exception Handler in Spring Boot.
๐ฏ Goals
- Create centralized exception handling using
@ControllerAdvice - Send consistent error responses
- Support custom exceptions like
ResourceNotFoundException - Handle validation and internal server errors
๐งฑ Folder Structure (Recommended)
src/main/java/com/example/demo
โโโ controller
โ โโโ ProductController.java
โโโ dto
โ โโโ ErrorResponse.java
โโโ exception
โ โโโ GlobalExceptionHandler.java
โ โโโ ResourceNotFoundException.java
โ โโโ ValidationException.java (optional)
โโโ DemoApplication.java
๐ง Step 1: Create Custom Exception Classes
โ ResourceNotFoundException
package com.example.demo.exception;
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
๐น Use one exception per business rule for clarity.
๐งพ Step 2: Standardized Error Response DTO
package com.example.demo.dto;
import java.time.LocalDateTime;
public class ErrorResponse {
private LocalDateTime timestamp;
private int status;
private String error;
private String message;
private String path;
public ErrorResponse(int status, String error, String message, String path) {
this.timestamp = LocalDateTime.now();
this.status = status;
this.error = error;
this.message = message;
this.path = path;
}
// Getters and Setters
}
๐น Always provide
timestamp,status, andpathfor better traceability.
๐ Step 3: Global Exception Handler (@ControllerAdvice)
package com.example.demo.exception;
import com.example.demo.dto.ErrorResponse;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import java.util.stream.Collectors;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex, HttpServletRequest request) {
ErrorResponse error = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
HttpStatus.NOT_FOUND.getReasonPhrase(),
ex.getMessage(),
request.getRequestURI()
);
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex, HttpServletRequest request) {
String errorMsg = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(e -> e.getField() + ": " + e.getDefaultMessage())
.collect(Collectors.joining(", "));
ErrorResponse error = new ErrorResponse(
HttpStatus.BAD_REQUEST.value(),
"Validation Failed",
errorMsg,
request.getRequestURI()
);
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneric(Exception ex, HttpServletRequest request) {
ErrorResponse error = new ErrorResponse(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(),
ex.getMessage(),
request.getRequestURI()
);
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
๐จ Pro Tip: Always add a fallback
@ExceptionHandler(Exception.class)to handle uncaught runtime exceptions.
๐ Step 4: Sample Controller
package com.example.demo.controller;
import com.example.demo.exception.ResourceNotFoundException;
import jakarta.validation.constraints.Min;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/products")
@Validated
public class ProductController {
@GetMapping("/{id}")
public String getProductById(@PathVariable @Min(1) int id) {
if (id != 1) {
throw new ResourceNotFoundException("Product with ID " + id + " not found.");
}
return "Product ID: " + id;
}
}
โ You can now test
/products/99and see the formatted JSON error.
๐ค Sample Error Response
๐ซ /products/999
{
"timestamp": "2025-06-16T10:25:48.709",
"status": 404,
"error": "Not Found",
"message": "Product with ID 999 not found.",
"path": "/products/999"
}
๐ซ /products/-1
{
"timestamp": "2025-06-16T10:26:14.109",
"status": 400,
"error": "Validation Failed",
"message": "id: must be greater than or equal to 1",
"path": "/products/-1"
}
๐งผ Logging (Optional but Recommended)
For production, log exceptions using a logging framework (e.g., SLF4J + Logback):
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneric(Exception ex, HttpServletRequest request) {
logger.error("Unhandled exception occurred", ex);
...
}
โ Best Practices Checklist
- [x] Use
@ControllerAdviceto centralize error handling. - [x] Create reusable, specific custom exceptions.
- [x] Never leak stack traces to clients.
- [x] Use proper HTTP codes: 404 for missing data, 400 for validation, 500 for generic.
- [x] Always include request URI and timestamp in response.
๐ฆ Bonus: Extend with Custom Exception Interface
If you want even cleaner control:
public interface ApiException {
HttpStatus getStatus();
String getErrorCode(); // optional for localization or tracking
}
Your custom exception can implement this for dynamic mapping.
๐ Final Thoughts
Global exception handling isn't just about catching errors โ it's about designing fault-tolerant, debuggable, and user-friendly APIs. This pattern is a must-have in any Spring Boot project going into production.
Top comments (0)