In this article we will see how to implement exception handling in Spring web services in Java. We will take you through the different options you have in Spring to handle error scenarios and return a determined response for each of them.
We will also cover another more basic way to implement error handling without the implicit need of Spring components.
Let’s start!
Introduction
Error handling in REST services has always been a bit controversial. The main reason for this is because there are different approaches that could be taken, each of them with their own advantages and disadvantages. The fact that some of the pros and cons for each of them are not completely objective in some cases, is something that doesn’t help to tackle these arguments.
We are going to take you through different ways of handling error scenarios in your Spring REST services, trying to be as objective as we could possibly be. Our goal is for you to judge and decide what approach suits you best.
Before we get into the possible ways of handling exceptions, let’s set the grounds for our first few examples.
In our first few examples we are going to use a fake CustomersRepository
that uses the following interface:
public interface CustomersRepository {
/**
* Creates a customer and returns the customer including the generated ID
*/
Customer create(Customer customer);
/**
* Finds an existing customer or returns CustomerNotFoundException
*/
Customer findById(Long id);
}
You can see how we return a Customer
in both cases, what happens if something does not go as expected? We will be throwing an exception and handle it accordingly.
If you need help to bootstrap a simple Spring Boot application to run these examples, you can check our article “How to Bootstrap a Spring Boot Application”.
Let’s start!

Controller-Based Exception Handling
One of the most basic options to handle exceptions in Spring is by using @ExceptionHandler annotation at controller level.
How can we do that? Let’s look at a CustomerController
with two basic endpoints, we will add a method to handle the case where a customer has not been found. It would look like this:
@RestController
@RequestMapping("/api/customers")
public class CustomerController {
private final CustomersRepository repository;
public CustomerController(CustomersRepository repository) {
this.repository = repository;
}
@PostMapping
ResponseEntity<String> createCustomer(@RequestBody Customer customer, UriComponentsBuilder uriBuilder) {
final Customer newCustomer = repository.create(customer);
return ResponseEntity
.created(uriBuilder
.path("/api/customers/{id})")
.buildAndExpand(newCustomer.id())
.toUri()
).build();
}
@GetMapping("/{id}")
ResponseEntity<Customer> findCustomer(@PathVariable("id") Long id) {
final Customer customer = repository.findById(id);
return ResponseEntity.ok(customer);
}
@ExceptionHandler(CustomerNotFound.class)
ResponseEntity<String> customerNotFound(CustomerNotFound exception) {
return ResponseEntity.notFound().build();
}
}
In this case, our repository will return CustomerNotFound
exception if the customer does not exist in our database. The exception has been previously declared in this way:
public class CustomerNotFound extends Exception{
public CustomerNotFound(String message) {
super(message);
}
}
In order to be able to handle this exception when it gets bubbled up by our application, we have to define a handler for it in our controller. We do so by using @ExceptionHandler annotation.
By using @ExceptionHandler we map a custom response to a given exception, in this case the exception given is CustomerNotFound
.
There is another way to do something similar, this is by using @ResponseStatus annotation.
The @ResponseStatus annotation will map our response to the corresponding status code, so in our example it’d be the equivalent of returning ResponseEntity.notFound()
.
Our exception handler method will be slight different in this case:
@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "Customer could not be found!")
@ExceptionHandler(CustomerNotFound.class)
void customerNotFound(CustomerNotFound exception) {}
Some of the advantages of this method is that in terms of readability or maintainability, it’s easy to find how is an exception handled in the current controller and what’s the configured response when this exception happens. It’s also a concise way to define exception handling in our code.
On the other hand, the use of controller-based exception handling does not allow the reuse of these handlers or even configure global exception handlers that would be applied to all of our controllers.
Let’s look at another option now.
Using @ControllerAdvice
Another way of defining exception handlers is by using @ControllerAdvice annotation. This will allows us to define global exception handling in our application, let’s see how it would look like:
@ControllerAdvice
public class GlobalExceptionHandler {
@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "Customer could not be found!")
@ExceptionHandler(CustomerNotFound.class)
void customerNotFound(CustomerNotFound exception) {}
}
If we try to fetch a non-existing customer using curl, we will also get a 404, exactly in the same way as we did when we saw the controller-based exception handling.
curl -v http://localhost:8080/api/customers/23
* Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /api/customers/23 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.79.1
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 404
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Tue, 29 Nov 2022 07:23:16 GMT
<
{"timestamp":"2022-11-29T07:23:16.265+00:00","status":404,"error":"Not Found","trace":"CustomerNotFound(message=Could not find customer 23, id=23)
...
This is probably the most used method to define exception handling in Spring Boot. Its main advantage is that we don’t have to define an exception handler in each controller and we just have to define it once in our code and it’ll be reused by each controller.
On the other hand, the main disadvantage is that to be able to know where the application is configuring exception handling, we need a good understanding of Spring and we will have to search for @ControllerAdvice to find the place where it’s been configured. Therefore, in terms of readability this approach is not the best for people not very familiar with the code or with Spring in general.
One more thing we haven’t mentioned before is how Spring handles these two approaches internally. If we look at our logs we will be able to see something similar to this:
2022-11-29 07:29:56.196 WARN 36520 --- [nio-8080-exec-2] .m.m.a.ExceptionHandlerExceptionResolver : Resolved [CustomerNotFound(message=Could not find customer 123, id=123)]
As you can see, Spring calls ExceptionHandlerExceptionResolver
to resolve configured exceptions. Let’s know a bit more about this component.
ExceptionHandlerExceptionResolver
This component is responsible for resolving all the exceptions defined through @ExceptionHandler annotations, therefore it is a very important component in Spring exception handling!
This component also extends from another known component, AbstractHandlerMethodExceptionResolver
. This components extends from HandlerExceptionResolver
interface, and this allows us defining our own custom exception resolvers.
This method is not used very frequently, as there are now better ways to do it. However, if you still want to take a look at an example implementing a custom exception resolver, you can find it here.
We’ve seen the main methods to handle exceptions that Spring provides at the moment, but is there any other way? We think there is one more way that, as usual, has its pros and cons. Let’s see how!
Custom Way Using Monads
There are some views claiming that Spring does “too much magic”, therefore, we lose track of what’s happening under the covers in our application. For those feeling in this side, we have one more proposal. Let’s start!
Introducing Either for Controller-Based Handling
The first we are going to do is to change our CustomerRepository
interface to return an Either
monad instead.
We could’ve used Arrow’s Either in this example, but we’ve decided to create a custom Either
class for this example. It look like this:
public class Either<F, S> {
static class Success<F, S> extends Either<F, S> {
private final S s;
public Success(S s) {
this.s = s;
}
S entity() {
return s;
}
}
static class Failure<F, S> extends Either<F, S> {
private final F f;
public Failure(F f) {
this.f = f;
}
F exception() {
return f;
}
}
private Either() {}
}
As you can see, it’s quite simple. We’ll see how to use it with an example.
First of all, we are going to modify a custom InMemoryCustomerRepository
implementation to make use of our Either
class.
@Component("inMemoryCostumersRepository")
public class InMemoryCustomersRepository implements CustomersRepository {
private final Map<Long, Customer> customers = new HashMap<>();
@Override
public Either<Exception, Customer> create(Customer customer) {
final Customer newCustomer = new Customer((long) customers.size(), customer.name(), customer.age());
customers.put(newCustomer.id(), newCustomer);
return new Either.Success<>(newCustomer);
}
@Override
public Either<Exception, Customer> findById(Long id) {
final Customer existing = customers.get(id);
if (existing != null) {
return new Either.Success<>(existing);
}
return new Either.Failure<>(new CustomerNotFound("Could not find customer " + id));
}
}
We are going to replace any Spring-based configuration responsible for exception handling and define the responses ourselves, let’s see how it’d look like after applying these changes in our controller. It’s worth mentioning that some of the current limitations of Java 17 make these changes slightly less simple than the Kotlin solution we saw in our article “Exception handling in Spring REST services in Kotlin”. For instance, pattern matching for switch statements is not yet available in this version. However, it was included in Java 18, you can check these notes here.
This is forcing us to define a type
method which returns an enum type, to be able to compare the result using a switch statement.
@RestController
@RequestMapping("/api/customers")
public class CustomerController {
private final CustomersRepository repository;
public CustomerController(CustomersRepository repository) {
this.repository = repository;
}
@PostMapping
ResponseEntity<String> createCustomer(@RequestBody Customer customer, UriComponentsBuilder uriBuilder) {
final Either<Exception, Customer> result = repository.create(customer);
return switch (result.type()) {
case SUCCESS -> ResponseEntity
.created(uriBuilder
.path("/api/customers/{id})")
.buildAndExpand(((Either.Success<Exception, Customer>) result).entity().id())
.toUri()
).build();
case FAILURE -> ResponseEntity.internalServerError().build();
};
}
@GetMapping("/{id}")
ResponseEntity<Customer> findCustomer(@PathVariable("id") Long id) {
Either<Exception, Customer> result = repository.findById(id);
return switch (result.type()) {
case SUCCESS -> ResponseEntity.ok(((Either.Success<Exception, Customer>) result).entity());
case FAILURE -> ResponseEntity.internalServerError().build();
};
}
}
Another limitation in the Java code if we compare it to Kotlin is the absence of “smart casting” in switch statements, this is forcing us to manually cast the corresponding Either
response to success or failure for each case.
The main benefit of this approach is that we don’t rely on bubbling up exceptions, the whole exception handling is within our code, so we can easily read and understand how it’ll behave.
As we saw earlier for controller-based exception handling in Spring, this approach has the same drawback, we cannot reuse it at the moment for all of our controllers and we’d have to define it in every single endpoint. Can we do something to address this? Let’s try!
Global exception handling using Either
We will try to modify our Either
class to make it available to use for “global” exception handling. The goal is to provide a “default” configuration for our responses, but at the same time we are able to override the behaviour at controller level. The solution will be very similar to our Kotlin solution, although the solution in Java is a bit more verbose than in Kotlin, as expected.
The resulting Either
class will look like this:
public abstract class Either<F, S> {
protected Function<Either.Success<F, S>, ResponseEntity<S>> successHandler = (e) -> ResponseEntity.ok(e.entity());
protected Function<Either.Failure<F, S>, ResponseEntity<F>> failureHandler = (e) -> ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.exception());
abstract EitherType type();
abstract <T> ResponseEntity<T> response();
static final class Success<F, S> extends Either<F, S> {
private final S s;
public Success(S s) {
this.s = s;
}
S entity() {
return s;
}
@Override
EitherType type() {
return SUCCESS;
}
@Override
ResponseEntity<S> response() {
return this.successHandler.apply(this);
}
}
static final class Failure<F, S> extends Either<F, S> {
private final F f;
public Failure(F f) {
this.f = f;
}
F exception() {
return f;
}
@Override
EitherType type() {
return FAILURE;
}
@Override
ResponseEntity<F> response() {
return this.failureHandler.apply(this);
}
}
public Either<F, S> onFailureDo(Function<Failure<F, S>, ResponseEntity<F>> action) {
this.failureHandler = action;
return this;
}
public Either<F, S> onSuccessDo(Function<Success<F, S>, ResponseEntity<S>> action) {
this.successHandler = action;
return this;
}
enum EitherType {
SUCCESS,
FAILURE
}
private Either() {}
}
There are a few things to highlight in our new implementation, we will look at each of them step by step to make it easier for you to understand.
The first thing to notice is that we are now providing a response
method to be able to convert our Either
class to a Spring ResponseEntity
object.
abstract <T> ResponseEntity<T> response();
Each of the Either
subclasses, Success
and Failure
, will override this method to be able to map our either to the corresponding http response, including status code and the presence or absence of a body.
static final class Success<F, S> extends Either<F, S> {
private final S s;
public Success(S s) {
this.s = s;
}
S entity() {
return s;
}
@Override
EitherType type() {
return SUCCESS;
}
@Override
ResponseEntity<S> response() {
return this.successHandler.apply(this);
}
}
static final class Failure<F, S> extends Either<F, S> {
private final F f;
public Failure(F f) {
this.f = f;
}
F exception() {
return f;
}
@Override
EitherType type() {
return FAILURE;
}
@Override
ResponseEntity<F> response() {
return this.failureHandler.apply(this);
}
}
You will now notice that these two classes call two different handlers instead of returning a ResponseEntity
object directly. Why is that? Well, we’ve decided to do that to be able to provide a default response but also allow overriding the default response in our controllers.
You might be wondering now, what’s the default configuration?
We initialise both successHandler
and errorHandler
with our default configuration like this:
protected Function<Either.Success<F, S>, ResponseEntity<S>> successHandler = (e) ->
ResponseEntity.ok(e.entity());
protected Function<Either.Failure<F, S>, ResponseEntity<F>> failureHandler = (e) ->
ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.exception());
As you can see, any successful response will be transformed to a 200 (OK) response including the entity in the body, if present. On the other hand, any failure will be transformed by default to a 500 (SERVER ERROR) response including the exception in the body.
This is just a simple example, it’s up to you what exactly do you want to include the body in case of failure. For example, you could include the message included in the exception.
Now that we understand how the default behaviour is set, how can we override this behaviour for a specific case? To allow this, we have provided these two methods:
public Either<F, S> onFailureDo(Function<Failure<F, S>, ResponseEntity<F>> action) {
this.failureHandler = action;
return this;
}
public Either<F, S> onSuccessDo(Function<Success<F, S>, ResponseEntity<S>> action) {
this.successHandler = action;
return this;
}
You can see how these methods accept a function that receives an Either.Success
or an Either.Failure
and return a ResponseEntity
object with the corresponding failure or success type.
How can we use all of this then? Let’s look at our new CustomerController
after having integrated this.
@RestController
@RestController
@RequestMapping("/api/customers")
public class CustomerController {
private final CustomersRepository repository;
public CustomerController(CustomersRepository repository) {
this.repository = repository;
}
@PostMapping
ResponseEntity<String> createCustomer(@RequestBody Customer customer, UriComponentsBuilder uriBuilder) {
return repository.create(customer)
.onSuccessDo(e -> {
return ResponseEntity
.created(uriBuilder
.path("/api/customers/{id})")
.buildAndExpand(e.entity().id())
.toUri()
).build();
}).response();
}
@GetMapping("/{id}")
ResponseEntity<Customer> findCustomer(@PathVariable("id") Long id) {
return repository.findById(id)
.onFailureDo(e -> {
if (e.exception() instanceof CustomerNotFound) {
return ResponseEntity.notFound().build();
} else {
return ResponseEntity.internalServerError().build();
}
}).response();
}
}
You can see how our controller now uses the default configuration for failures in the createCustomer
method and for success in findCustomer
method. For example, findCustomer
will then return 200 (OK) response and include the customer in the json body.
However, we override the behaviour for success in createCustomer
method and for failure in findCustomer
method. Why is that? In our particular case, creating a customer successfully implies returning 201 (CREATED) with a Location
header, as we’ve seen earlier in this article. We also need a different behaviour for failures in findCustomer
to be able to handle CustomerNotFound
properly.
That’s quite nice, right? The main advantage of this approach is that we are now able to provide a global configuration for exception handling but at the same time being able to easily read and understand how is exception handling configured in our code, no Spring “magic” involved!
If you are interested in learning Spring in depth, we highly recommend the following books:

Conclusion
In this article we have seen the different ways to handle exceptions in a Spring Boot application. As we said before, exception handling is a very controversial topic, as there are different opinions around what approach works best in our applications. Hopefully we’ve been able to bring an objective and impartial view about this topic and you could now use it to make your own assumptions and take the right decisions in your projects.
That’s all from us today! We really hope you’ve enjoyed this article as much as we enjoyed writing it!
Looking forward to seeing you with us again soon! Thanks for reading us!