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:
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:
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:
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:
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:
Get started with Spring and Spring Boot, through the Learn Spring course:
>> LEARN SPRINGExplore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:
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:
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:
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.
1. Overview
In this tutorial, weβll look at various strategies available for handling errors in a Spring WebFlux project while walking through a practical example.
Weβll also point out where it might be advantageous to use one strategy over another and provide a link to the full source code at the end.
2. Setting Up the Example
The Maven setup is the same as our previous article, which provides an introduction to Spring WebFlux.
For our example, weβll use a RESTful endpoint that takes a username as a query parameter and returns βHello usernameβ as a result.
First, letβs create a router function that routes the /hello request to a method named handleRequest in the passed-in handler:
@Bean
public RouterFunction<ServerResponse> routes(Handler handler) {
return RouterFunctions.route(RequestPredicates.GET("/hello")
.and(RequestPredicates.accept(MediaType.TEXT_PLAIN)),
handler::handleRequest);
}
Next, weβll define the handleWithGlobalErrorHandler() method that calls the sayHello() method and finds a way of including/returning its result in the ServerResponse body:
public Mono<ServerResponse> handleWithGlobalErrorHandler(ServerRequest request) {
return
//...
sayHello(request)
//...
}
Finally, the sayHello() method is a simple utility method that concatenates the βHelloβ String and the username:
private Mono<String> sayHello(ServerRequest request) {
try {
return Mono.just("Hello, " + request.queryParam("name").get());
} catch (Exception e) {
return Mono.error(e);
}
}
So long as a username is present as part of our request, e.g., if the endpoint is called as β/hello?username=Tonniβ, this endpoint will always function correctly.
However, if we call the same endpoint without specifying a username, e.g., β/helloβ, it will throw an exception.
Below, weβll look at where and how we can reorganize our code to handle this exception in WebFlux.
3. Handling Errors at a Functional Level
There are two key operators built into the Mono and Flux APIs to handle errors at a functional level.
Letβs briefly explore them and their usage.
3.1. Handling Errors With onErrorReturn
We can use onErrorReturn() to return a static default value whenever an error occurs:
public Mono<ServerResponse> handleWithErrorReturn(ServerRequest request) {
return sayHello(request)
.onErrorReturn("Hello Stranger")
.flatMap(s -> ServerResponse.ok()
.contentType(MediaType.TEXT_PLAIN)
.bodyValue(s));
}
Here weβre returning a static βHello Strangerβ whenever the buggy concatenation function sayHello() throws an exception.
3.2. Handling Errors With onErrorResume
There are three ways that we can use onErrorResume to handle errors:
- Compute a dynamic fallback value
- Execute an alternative path with a fallback method
- Catch, wrap and re-throw an error, e.g., as a custom business exception
Letβs see how we can compute a value:
public Mono<ServerResponse> handleWithErrorResumeAndDynamicFallback(ServerRequest request) {
return sayHello(request)
.flatMap(s -> ServerResponse.ok()
.contentType(MediaType.TEXT_PLAIN)
.bodyValue(s))
.onErrorResume(e -> Mono.just("Error " + e.getMessage())
.flatMap(s -> ServerResponse.ok()
.contentType(MediaType.TEXT_PLAIN)
.bodyValue(s)));
}
Here weβre returning a String consisting of the dynamically obtained error message appended to the string βErrorβ whenever sayHello() throws an exception.
Next, letβs call a fallback method when an error occurs:
public Mono<ServerResponse> handleWithErrorResumeAndFallbackMethod(ServerRequest request) {
return sayHello(request)
.flatMap(s -> ServerResponse.ok()
.contentType(MediaType.TEXT_PLAIN)
.bodyValue(s))
.onErrorResume(e -> sayHelloFallback()
.flatMap(s -> ServerResponse.ok()
.contentType(MediaType.TEXT_PLAIN)
.bodyValue(s)));
}
Here weβre calling the alternative method sayHelloFallback() whenever sayHello() throws an exception.
The final option using onErrorResume() is to catch, wrap and re-throw an error, e.g., as a NameRequiredException:
public Mono<ServerResponse> handleWithErrorResumeAndCustomException(ServerRequest request) {
return ServerResponse.ok()
.body(sayHello(request)
.onErrorResume(e -> Mono.error(new NameRequiredException(
HttpStatus.BAD_REQUEST,
"username is required", e))), String.class);
}
Here weβre throwing a custom exception with the message βusername is requiredβ whenever sayHello() throws an exception.
4. Handling Errors at a Global Level
So far, all the examples weβve presented have tackled error handling at a functional level. It is important to not that the error handling at functional level is applicable for both annotation based configuration as well as with the RouterFunction we used in this article.
Certainly we can use annotations to handle exceptions for our REST API but for this article weβll opt to handle our WebFlux errors at a global level with a global error handler. To do this, we only need to take two steps:
- Customize the Global Error Response Attributes
- Implement the Global Error Handler
The exception that our handler throws will be automatically translated to an HTTP status and a JSON error body.
To customize these, we can simply extend the DefaultErrorAttributes class and override its getErrorAttributes() method:
public class GlobalErrorAttributes extends DefaultErrorAttributes{
@Override
public Map<String, Object> getErrorAttributes(ServerRequest request,
ErrorAttributeOptions options) {
Map<String, Object> map = super.getErrorAttributes(
request, options);
map.put("status", HttpStatus.BAD_REQUEST);
map.put("message", "username is required");
return map;
}
}
Here we want the status: BAD_REQUEST and the message βusername is requiredβ returned as part of the error attributes when an exception occurs.
Next, letβs implement the Global Error Handler.
For this, Spring provides a convenient AbstractErrorWebExceptionHandler class for us to extend and implement in handling global errors:
@Component
@Order(-2)
public class GlobalErrorWebExceptionHandler extends
AbstractErrorWebExceptionHandler {
// constructors
@Override
protected RouterFunction<ServerResponse> getRoutingFunction(
ErrorAttributes errorAttributes) {
return RouterFunctions.route(
RequestPredicates.all(), this::renderErrorResponse);
}
private Mono<ServerResponse> renderErrorResponse(
ServerRequest request) {
Map<String, Object> errorPropertiesMap = getErrorAttributes(request,
ErrorAttributeOptions.defaults());
return ServerResponse.status(HttpStatus.BAD_REQUEST)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(errorPropertiesMap));
}
}
In this example, we set the order of our global error handler to -2. This is to give it a higher priority than the DefaultErrorWebExceptionHandler, which is registered at @Order(-1).
The errorAttributes object will be the exact copy of the one that we pass in the Web Exception Handlerβs constructor. This should ideally be our customized Error Attributes class.
Then weβre clearly stating that we want to route all error handling requests to the renderErrorResponse() method.
Finally, we get the error attributes and insert them inside a server response body.
This then produces a JSON response with details of the error, the HTTP status, and the exception message for machine clients. For browser clients, it has a βwhite-labelβ error handler that renders the same data in HTML format. This can be customized, of course.
5. Conclusion
In this article, we looked at various strategies available for handling errors in a Spring WebFlux project and pointed out where it might be advantageous to use one strategy over another.
