🌱 Spring Boot Q56 / 69

How to write custom exceptions in spring boot.

AI-Powered Answer ✓ Answered

Custom exceptions are essential for providing specific error handling and clearer communication of issues within a Spring Boot application. This guide covers how to define, handle, and utilize custom exceptions effectively, along with best practices for structuring error responses.

1. Defining a Simple Custom Exception

To create a custom exception, extend RuntimeException (for unchecked exceptions) or Exception (for checked exceptions). Unchecked exceptions are generally preferred in Spring Boot for simpler code, as they don't require explicit throws declarations.

java
package com.example.exceptions;

public class ResourceNotFoundException extends RuntimeException {

    public ResourceNotFoundException(String message) {
        super(message);
    }

    public ResourceNotFoundException(String message, Throwable cause) {
        super(message, cause);
    }
}

2. Structuring Custom Error Responses (DTO)

For a consistent and structured error response format, it's good practice to define a custom error response DTO (Data Transfer Object). This allows clients to easily parse error details.

java
package com.example.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(LocalDateTime timestamp, int status, String error, String message, String path) {
        this.timestamp = timestamp;
        this.status = status;
        this.error = error;
        this.message = message;
        this.path = path;
    }

    // Getters (and setters, if needed, omitted for brevity)
    public LocalDateTime getTimestamp() { return timestamp; }
    public int getStatus() { return status; }
    public String getError() { return error; }
    public String getMessage() { return message; }
    public String getPath() { return path; }
}

3. Global Exception Handling with @ControllerAdvice

For a centralized approach to handling exceptions across all controllers, use @ControllerAdvice combined with @ExceptionHandler. This allows you to define specific HTTP responses and custom error bodies for different exception types.

java
package com.example.handlers;

import com.example.dto.ErrorResponse;
import com.example.exceptions.ResourceNotFoundException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.WebRequest;

import java.time.LocalDateTime;

@ControllerAdvice
public class GlobalExceptionHandler {

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

        HttpStatus status = HttpStatus.NOT_FOUND;
        ErrorResponse errorResponse = new ErrorResponse(
            LocalDateTime.now(),
            status.value(),
            status.getReasonPhrase(),
            ex.getMessage(),
            ((ServletWebRequest)request).getRequest().getRequestURI()
        );
        return new ResponseEntity<>(errorResponse, status);
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ErrorResponse> handleIllegalArgumentException(
            IllegalArgumentException ex, WebRequest request) {

        HttpStatus status = HttpStatus.BAD_REQUEST;
        ErrorResponse errorResponse = new ErrorResponse(
            LocalDateTime.now(),
            status.value(),
            status.getReasonPhrase(),
            ex.getMessage(),
            ((ServletWebRequest)request).getRequest().getRequestURI()
        );
        return new ResponseEntity<>(errorResponse, status);
    }

    // Generic handler for any other unhandled exceptions
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleAllExceptions(
            Exception ex, WebRequest request) {

        HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
        ErrorResponse errorResponse = new ErrorResponse(
            LocalDateTime.now(),
            status.value(),
            status.getReasonPhrase(),
            "An unexpected error occurred: " + ex.getMessage(),
            ((ServletWebRequest)request).getRequest().getRequestURI()
        );
        return new ResponseEntity<>(errorResponse, status);
    }
}

4. Using Custom Exceptions in Controllers

Once defined, you can throw your custom exceptions from your service or controller methods. The GlobalExceptionHandler will automatically intercept and process them, returning the appropriate HTTP status and error response body.

java
package com.example.controllers;

import com.example.exceptions.ResourceNotFoundException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyController {

    @GetMapping("/api/resources/{id}")
    public String getResource(@PathVariable Long id) {
        if (id < 1) {
            throw new IllegalArgumentException("ID must be positive.");
        }
        if (id == 99L) {
            throw new ResourceNotFoundException("Resource with ID " + id + " not found.");
        }
        return "Resource with ID: " + id;
    }
}

5. Alternative: Using @ResponseStatus Directly on the Exception

For simpler custom exceptions where you always want to return a specific HTTP status code, you can annotate the exception class itself with @ResponseStatus. This removes the need for an explicit @ExceptionHandler method in ControllerAdvice for that specific exception.

java
package com.example.exceptions;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.BAD_REQUEST) // Sets HTTP 400 Bad Request
public class InvalidInputException extends RuntimeException {
    public InvalidInputException(String message) {
        super(message);
    }
    public InvalidInputException(String message, Throwable cause) {
        super(message, cause);
    }
}

When InvalidInputException is thrown, Spring will automatically return an HTTP 400 status. Note that while convenient, using @ControllerAdvice provides more granular control over the response body format, allowing you to return your custom ErrorResponse DTO consistently for all errors.