eBook – Guide Spring Cloud – NPI EA (cat=Spring Cloud)
announcement - icon

Let's get started with a Microservice Architecture with Spring Cloud:

>> Join Pro and download the eBook

eBook – Mockito – NPI EA (tag = Mockito)
announcement - icon

Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.

Get started with mocking and improve your application tests using our Mockito guide:

Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Reactive – NPI EA (cat=Reactive)
announcement - icon

Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:

>> Join Pro and download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Jackson – NPI EA (cat=Jackson)
announcement - icon

Do JSON right with Jackson

Download the E-book

eBook – HTTP Client – NPI EA (cat=Http Client-Side)
announcement - icon

Get the most out of the Apache HTTP Client

Download the E-book

eBook – Maven – NPI EA (cat = Maven)
announcement - icon

Get Started with Apache Maven:

Download the E-book

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

eBook – RwS – NPI EA (cat=Spring MVC)
announcement - icon

Building a REST API with Spring?

Download the E-book

Course – LS – NPI EA (cat=Jackson)
announcement - icon

Get started with Spring and Spring Boot, through the Learn Spring course:

>> LEARN SPRING
Course – RWSB – NPI EA (cat=REST)
announcement - icon

Explore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:

>> The New “REST With Spring Boot”

Course – LSS – NPI EA (cat=Spring Security)
announcement - icon

Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.

I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.

You can explore the course here:

>> Learn Spring Security

Course – LSD – NPI EA (tag=Spring Data JPA)
announcement - icon

Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.

Get started with Spring Data JPA through the guided reference course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (cat=Spring Boot)
announcement - icon

Refactor Java code safely — and automatically — with OpenRewrite.

Refactoring big codebases by hand is slow, risky, and easy to put off. That’s where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.

Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions — one for newcomers and one for experienced users. You’ll see how recipes work, how to apply them across projects, and how to modernize code with confidence.

Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.

Course – LJB – NPI EA (cat = Core Java)
announcement - icon

Code your way through and build up a solid, practical foundation of Java:

>> Learn Java Basics

1. Introduction

In this tutorial, we’ll explore different error response formats in the Spring framework. We’ll also understand how to raise and handle RFC7807 ProblemDetail with custom attributes, as well as how to raise custom exceptions in Spring WebFlux.

2. Exception Response Formats in Spring Boot 3

Let’s understand the various error response formats supported out-of-the-box.

By default, Spring Framework provides the DefaultErrorAttributes class that implements the ErrorAttributes interface to generate an error response in the event of an unhandled error. In the case of a default error, the system generates a JSON response structure that we can examine more closely:

{
    "timestamp": "2023-04-01T00:00:00.000+00:00",
    "status": 500,
    "error": "Internal Server Error",
    "path": "/api/example"
}

While this error response contains a few key attributes, it may not be beneficial in investigating the issue. Fortunately, we can modify this default behavior by creating a custom implementation of the ErrorAttributes interface in our Spring WebFlux application.

Starting with Spring Framework 6 ProblemDetail, representation for an RFC7807 specification is supported. ProblemDetail includes a few standard attributes that define error details, also an option to extend the details for customization. The supported attributes are listed below:

  • type (string) – a URI reference that identifies the problem type
  • title (string) – short summary of the problem type
  • status (number) – the HTTP status code
  • detail (string) – should contain the details of the exception.
  • instance (string) – a URI reference to identify the specific reason for the issue. For instance, it can refer to the attribute which caused the issue.

In addition to the standard attributes mentioned above, ProblemDetail also contains a Map<String, Object> to add custom parameters to provide more detailed information about the issue.

Let’s take a look at a sample error response structure with custom object errors:

{
  "type": "https://example.com/probs/email-invalid",
  "title": "Invalid email address",
  "detail": "The email address 'john.doe' is invalid.",
  "status": 400,
  "timestamp": "2023-04-07T12:34:56.789Z",
  "errors": [
    {
      "code": "123",
      "message": "Error message",
      "reference": "https//error/details#123"
    }
  ]
}

Spring Framework also provides a base implementation called ErrorResponseException. This exception encapsulates a ProblemDetail object, which generates additional information about the error that occurred. We can extend this exception to customize and add attributes.

3. How to Implement ProblemDetail RFC 7807 Exception

Although Spring 6+ / Spring Boot 3+ applications support the ProblemDetail exception by default, we need to enable it in one of the following ways.

3.1. Enable ProblemDetail Exception by Properties File

The ProblemDetail exception can be enabled by adding a property:

spring:
  mvc:
    problemdetails:
      enabled: true

3.2. Enable ProblemDetail Exception by Adding Exception Handler

The ProblemDetail exception can also be enabled by extending ResponseEntityExceptionHandler and adding a custom exception handler (even without any overrides):

@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
    //...
}

We’ll use this approach for this article, as we need to add custom exception handlers.

3.3. Implement ProblemDetail Exception

Let’s examine how to raise and handle a ProblemDetail exception with custom attributes by considering a straightforward application that provides a few endpoints for creating and retrieving User information.

Our controller has a GET /v1/users/{userId} endpoint that retrieves the user information based on the provided userId. If it cannot find any record, the code throws a simple custom exception called UserNotFoundException:

@GetMapping("/v1/users/{userId}")
public Mono<ResponseEntity<User>> getUserById(@PathVariable Long userId) {
    return Mono.fromCallable(() -> {
        User user = userMap.get(userId);
        if (user == null) {
            throw new UserNotFoundException("User not found with ID: " + userId);
        }
        return new ResponseEntity<>(user, HttpStatus.OK);
    });
}

Our UserNotFoundException extends RunTimeException:

public class UserNotFoundException extends RuntimeException {

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

Since we have a GlobalExceptionHandler custom handler that extends ResponseEntityExceptionHandler, ProblemDetail becomes the default exception format. To test this, we can try accessing the application with an unsupported HTTP method, for instance, POST, to view the exception format.

When a MethodNotAllowedException gets thrown, the ResponseEntityExceptionHandler will handle the exception and generate a response in the ProblemDetail format:

curl --location --request POST 'localhost:8080/v1/users/1'

This results in the ProblemDetail object as the response:

{
    "type": "about:blank",
    "title": "Method Not Allowed",
    "status": 405,
    "detail": "Supported methods: [GET]",
    "instance": "/users/1"
}

3.4. Extend ProblemDetail Exception with Custom Attributes in Spring WebFlux

Let’s extend the example by providing an exception handler for UserNotFoundException, which adds a custom object to the ProblemDetail response.

The ProblemDetail Object contains a properties attribute that accepts a String as a key and value as any Object.

We’ll add a custom object called ErrorDetails. This object contains the error code and message, as well as an error reference URL with additional details and instructions for resolving the issue:

@JsonSerialize(using = ErrorDetailsSerializer.class)
public enum ErrorDetails {
    API_USER_NOT_FOUND(123, "User not found", "http://example.com/123");
    @Getter
    private Integer errorCode;
    @Getter
    private String errorMessage;
    @Getter
    private String referenceUrl;

    ErrorDetails(Integer errorCode, String errorMessage, String referenceUrl) {
        this.errorCode = errorCode;
        this.errorMessage = errorMessage;
        this.referenceUrl = referenceUrl;
    }
}

To override the error behavior for UserNotException, we need to provide an error handler in the GlobalExceptionHandler class. This handler should set the API_USER_NOT_FOUND property of the ErrorDetails object, along with any other error details provided by the ProblemDetail object:

@ExceptionHandler(UserNotFoundException.class)
protected ProblemDetail handleNotFound(RuntimeException ex) {
    ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
    problemDetail.setTitle("User not found");
    problemDetail.setType(URI.create("https://example.com/problems/user-not-found"));
    problemDetail.setProperty("errors", List.of(ErrorDetails.API_USER_NOT_FOUND));
    return problemDetail;
}

We’ll also need an ErrorDetailsSerializer and ProblemDetailSerializer to customize the response format.

The ErrorDetailsSerializer is responsible for formatting our custom error object with error code, error message, and reference details:

public class ErrorDetailsSerializer extends JsonSerializer<ErrorDetails> {
    @Override
    public void serialize(ErrorDetails value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        gen.writeStartObject();
        gen.writeStringField("code", value.getErrorCode().toString());
        gen.writeStringField("message", value.getErrorMessage());
        gen.writeStringField("reference", value.getReferenceUrl());
        gen.writeEndObject();
    }
}

The ProblemDetailSerializer is responsible for formatting the overall ProblemDetail object along with the custom object(with the help of ErrorDetailsSerializer):

public class ProblemDetailsSerializer extends JsonSerializer<ProblemDetail> {

    @Override
    public void serialize(ProblemDetail value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        gen.writeStartObject();
        gen.writeObjectField("type", value.getType());
        gen.writeObjectField("title", value.getTitle());
        gen.writeObjectField("status", value.getStatus());
        gen.writeObjectField("detail", value.getDetail());
        gen.writeObjectField("instance", value.getInstance());
        gen.writeObjectField("errors", value.getProperties().get("errors"));
        gen.writeEndObject();
    }
}

Now, when we try to access the endpoint with an invalid userId, we should receive an error message with our custom attributes:

$ curl --location 'localhost:8080/v1/users/1'

This results in the ProblemDetail object along with the custom attribute:

{
  "type": "https://example.com/problems/user-not-found",
  "title": "User not found",
  "status": 404,
  "detail": "User not found with ID: 1",
  "instance": "/users/1",
  "errors": [
    {
      "errorCode": 123,
      "errorMessage": "User not found",
      "referenceUrl": "http://example.com/123"
    }
  ]
}

We can also use ErrorResponseException that implements ErrorResponse to expose HTTP status, response header, and body with the contract of RFC 7807 ProblemDetail.

In these examples, we have handled global exceptions using ResponseEntityExceptionHandler. Alternatively, AbstractErrorWebExceptionHandler can also be used to handle global Webflux exceptions.

4. Why Custom Exceptions

While the ProblemDetail format is helpful and flexible for adding custom attributes, there are situations where we may prefer to throw a custom error object that includes all details of the error. In such cases, using custom exceptions in Spring can provide a clear, more specific, and more consistent approach to handling errors and exceptions in our code.

5. Implement Custom Exceptions in Spring WebFlux

Let’s consider implementing a custom object as a response instead of ProblemDetail:

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class CustomErrorResponse {
    private String traceId;
    private OffsetDateTime timestamp;
    private HttpStatus status;
    private List<ErrorDetails> errors;
}

To throw this custom object, we need a custom exception:

public class CustomErrorException extends RuntimeException {
    @Getter
    private CustomErrorResponse errorResponse;

    public CustomErrorException(String message, CustomErrorResponse errorResponse) {
        super(message);
        this.errorResponse = errorResponse;
    }
}

Let’s create another version, v2 of the endpoint, which throws this custom exception. For simplicity, some of the fields, like traceId, are populated with random values:

@GetMapping("/v2/users/{userId}")
public Mono<ResponseEntity<User>> getV2UserById(@PathVariable Long userId) {
    return Mono.fromCallable(() -> {
        User user = userMap.get(userId);
        if (user == null) {
            CustomErrorResponse customErrorResponse = CustomErrorResponse
              .builder()
              .traceId(UUID.randomUUID().toString())
              .timestamp(OffsetDateTime.now().now())
              .status(HttpStatus.NOT_FOUND)
              .errors(List.of(ErrorDetails.API_USER_NOT_FOUND))
              .build();
            throw new CustomErrorException("User not found", customErrorResponse);
        }
        return new ResponseEntity<>(user, HttpStatus.OK);
    });
}

We need to add a handler in GlobalExceptionHandler to format the exception in the output response finally:

@ExceptionHandler({CustomErrorException.class})
protected ResponseEntity<CustomErrorResponse> handleCustomError(RuntimeException ex) {
    CustomErrorException customErrorException = (CustomErrorException) ex;
    return ResponseEntity.status(customErrorException.getErrorResponse().getStatus()).body(customErrorException.getErrorResponse());
}

Now if we try to access the endpoint with an invalid userId, we should get the error with custom attributes:

$ curl --location 'localhost:8080/v2/users/1'

This results in the CustomErrorResponse object as the response:

{
    "traceId": "e3853069-095d-4516-8831-5c7cfa124813",
    "timestamp": "2023-04-28T15:36:41.658289Z",
    "status": "NOT_FOUND",
    "errors": [
        {
            "code": "123",
            "message": "User not found",
            "reference": "http://example.com/123"
        }
    ]
}

6. Conclusion

In this article, we explored how to enable and use the ProblemDetail RFC7807 exception format provided by Spring Framework and learned how to create and handle custom exceptions in Spring WebFlux.

The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.
Baeldung Pro – NPI EA (cat = Baeldung)
announcement - icon

Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:

>> Explore a clean Baeldung

Once the early-adopter seats are all used, the price will go up and stay at $33/year.

eBook – HTTP Client – NPI EA (cat=HTTP Client-Side)
announcement - icon

The Apache HTTP Client is a very robust library, suitable for both simple and advanced use cases when testing HTTP endpoints. Check out our guide covering basic request and response handling, as well as security, cookies, timeouts, and more:

>> Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

Course – LS – NPI EA (cat=REST)

announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (tag=Refactoring)
announcement - icon

Modern Java teams move fast — but codebases don’t always keep up. Frameworks change, dependencies drift, and tech debt builds until it starts to drag on delivery. OpenRewrite was built to fix that: an open-source refactoring engine that automates repetitive code changes while keeping developer intent intact.

The monthly training series, led by the creators and maintainers of OpenRewrite at Moderne, walks through real-world migrations and modernization patterns. Whether you’re new to recipes or ready to write your own, you’ll learn practical ways to refactor safely and at scale.

If you’ve ever wished refactoring felt as natural — and as fast — as writing code, this is a good place to start.

Course – LS – NPI (cat=REST)
announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

eBook Jackson – NPI EA – 3 (cat = Jackson)