Error Handling in Spring Webflux
Look at error handling in Spring Webflux.
Join the DZone community and get the full member experience.Join For Free
The topic of error handling in web applications is very important. From a client perspective it is essential to know on how was the request proceeded and in case of any error is crucial to provide to the client a valid reason, especially if the error was due to the client’s actions. There are different situations, when notifying callers about concrete reasons is important – think about server-side validations, business logic errors that come due to bad requests or simple not found situations.
The mechanism of error handling in Webflux is different, from what we know from Spring MVC. Core building blocks of reactive apps –
Flux brings a special way to deal with error situations, and while old exception-based error handling still may work for some cases, it violates Spring Webflux nature. In this post I will do an overview of how to process errors in Webflux when it comes to business errors and absent data. I will not cover technical errors in this article, as they are handled by Spring framework.
When Do We Need to Handle Errors in Webflux
Before we will move to the subject of error handling, let define what we want to achieve. Assume, that we build application using a conventional architecture with a vertical separation by layers and a horizontal separation by domains. That means, that our app consists of three main layers: repositories (one that handle data access), services (one that do custom business logic) and handlers (to work with HTTP requests/responses; understand them as controllers in Spring MVC). Take a look on the graph below and you can note that potentially errors may occur on any layer:
Although, I need to clarify here, that from a technical perspective errors are not same. On the repository level (and let abstract here also clients, that deal with external APIs and all data-access components) usually occur what is called technical errors. For instance, something can be wrong with a database, so the repository component will throw an error. This error is a subclass of
RuntimeException and if we use Spring is handled by framework, so we don’t need to do something here. It will result to 500 error code.
An another case is what we called business errors. This is a violation of custom business rules of your app. While it may be considered as not as a good idea to have such errors, they are unavoidable, because, as it was mentioned before, we have to provide a meaningful response to clients in case of such violations. If you will return to the graph, you will note, that such errors usually occur on the service level, therefore we have to deal with them.
Now, let see how to handle errors and provide error responses in Spring Webflux APIs.
Start From Business Logic
In my previous post, I demonstrated how to build two-factor authentication for Spring Webflux REST API. That example follows the aforesaid architecture and is organized from repositories, services and handlers. As it was already mentioned, business errors take place inside a service. Prior to reactive Webflux, we often used exception-based error handling. It means that you provide a custom runtime exception (a subclass of
ResponseStatusException) which is mapped with specific http status.
However, Webflux approach is different. Main building blocks are
Flux components, that are chained throughout an app’s flow (note, from here I refer to both
Mono). Throwing an exception on any level will break an async nature. Also, Spring reactive repositories, such as
ReactiveMongoRepository don’t use exceptions to indicate an error situation.
Mono container provides a functionality to propagate error condition and empty condition:
Mono.empty()= this static method creates a
Monocontainer that completes without emitting any item
Mono.error()= this static method creates a
Monocontainer that terminates with an error immediately after being subscribed to
With this knowledge, we can now design a hypothetical login/signup flow to be able to handle situations, when 1) an entity is absent and 2) error occurred. If an error occurs on the repository level, Spring handles it by returning
Mono with error state. When the requested data is not found – empty
Mono. We also can add some validation for business rules inside the service. Take a look on the refactored code of signup flow from this post:
Now, we have a business logic. The next phase is to map results as HTTP responses on the handler level.
Display http Responses in Handlers
This level can correspond to the old good controllers in Spring MVC. The purpose of handlers is to work with HTTP requests and responses and by this to connect a business logic with the outer world. In the most simple implementation, the handler for signup process looks like this:
This code does essential things:
- Accepts a body payload from the HTTP request
- Call a business logic component
- Returns a result as HTTP response
However, it does not takes an advantage of custom error handling, that we talked about in the previous section. We need to handle a situation, when user already exists. For that, let refactor this code block:
Note, that compare to the previous post, I use here
bodyValue() instead of
body(). This is because
body method actually accepts producers, while
data object here is entity (
SignupResponse). For that I use
bodyValue() with passed value data. Read more on this here.
In this code we can specify to the client, what was a reason of the error. If user does exist already in database, we will provide
409 error code to the caller, so she/he have to use an another email address for signup procedure. That is what is about business errors. For technical errors our API displays
500 error code.
We can validate, that when we create a new user, everything works ok and the expected result is
200 success code:
On the other hand, if you will try to signup with the same email address, API should response with
400 error code, like it is shown on the screenshot below:
Moreover, we don’t need
success field for
SignupResponse entity anymore, as unsuccessful signup is handled with error codes. There is an another situation, I want to mention is this post – the problem of empty responses. This is what we would observe on a login example.
Special Case: Response on Empty Result
Why this is a special case? Well, technically, empty response is not an error, neither business error or technical error. There are different opinions among developers how to handle it properly. I think, that even it is not an exception in a traditional sense, we still need to expose
404 error code to the client, to demonstrate an absence of the requested information.
Let have a look on the login flow. For login flow is common a situation, opposite to the signup flow. For signup flow we have to ensure, that user does not exist yet, however for login we have to know that user does already exist. In the case of the absence we need to return an error response.
Take a look on the
login handler initial implementation:
From the service component’s perspective we could expect three scenarios:
- User exists and login is successful = return
- User exists but login was denied = return an error
- User does not exist = return an empty user
We have already seen how to work with errors in handlers. The empty response situation is managed using
switchIfEmpty method. Take a look on the refactored implementation:
Note, that unlike
switchIfEmpty accepts as an argument an alternative
Mono, rather than function. Now, let check that everything works as expected. The login for existed user entity and valid credentials should return a valid response:
When submitted credentials are wrong (password does not match), but user does exist (case no.2), we will obtain a
Bad request error code:
Finally, if repository is unable to find a user entity, handler will answer with
Please note, that this post is focused on the handler level. For what happens inside service, I recommend you to check the previous post and also to look on the complete source code in this github repository. If you have any questions – don’t hesitate to ask them in comments or contact me.
Published at DZone with permission of Yuri Mednikov. See the original article here.
Opinions expressed by DZone contributors are their own.