In this article we will see how to implement exception handling in Spring web services in Kotlin. 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. If you are interested in seeing the solution in Java, you can check our article in Java!
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:
interface CustomersRepository {
/**
* Creates a customer and returns the customer including the generated ID
*/
fun create(customer: Customer): Customer
/**
* Finds an existing customer or returns CustomerNotFoundException
*/
fun findById(id: Long): Customer
}
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")
class CustomerController(@Autowired val repository: CustomersRepository) {
@PostMapping
fun createCustomer(@RequestBody customer: Customer, uriBuilder: UriComponentsBuilder): ResponseEntity<String> {
val result = repository.create(customer)
return ResponseEntity
.created(uriBuilder.path("/api/customers/{id})").buildAndExpand(result.id).toUri())
.build()
}
@GetMapping("/{id}")
fun findCustomer(@PathVariable("id") id: Long): ResponseEntity<Customer> {
val result = repository.findById(id)
return ResponseEntity.ok(result)
}
@ExceptionHandler(CustomerNotFound::class)
fun customerNotFound(exception: CustomerNotFound): ResponseEntity<String> {
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:
data class CustomerNotFound(override val message: String? = null, val id: Long): Exception(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)
fun customerNotFound(exception: CustomerNotFound) {}
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
class GlobalExceptionHandler {
@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "Customer could not be found!")
@ExceptionHandler(CustomerNotFound::class)
fun customerNotFound(exception: CustomerNotFound) {}
}
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:
sealed class Either<F, S> {
data class Success<F, S>(private val s: S) : Either<F, S>() {
fun entity(): S = s
}
data class Failure<F, S>(private val f: F) : Either<F, S>() {
fun exception(): F = f
}
}
fun <E> E.failure() = Either.Failure<E, Nothing>(this)
fun <T> T.success() = Either.Success<Nothing, T>(this)
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")
class InMemoryCustomerRepository(val configuration: DatabaseConfiguration): CustomersRepository {
private val customers = mutableMapOf<Long, Customer>()
override fun create(customer: Customer): Either<Exception, Customer> {
val created = customer.copy(id = customers.keys.size.toLong())
customers[created.id!!] = created
return created.success() as Either<Exception, Customer>
}
override fun findById(id: Long): Either<Exception, Customer> {
return ((
customers[id]?.success() ?:
CustomerNotFound("Could not found customer $id", id).failure()
) as Either<Exception, Customer>)
}
}
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:
@RestController
@RequestMapping("/api/customers")
class CustomerController(@Autowired val repository: CustomersRepository) {
@PostMapping
fun createCustomer(@RequestBody customer: Customer, uriBuilder: UriComponentsBuilder): ResponseEntity<String> {
return when (val result = repository.create(customer)) {
is Either.Success -> {
ResponseEntity
.created(uriBuilder
.path("/api/customers/{id})")
.buildAndExpand(result.entity().id)
.toUri()
).build()
}
is Either.Failure -> {
ResponseEntity.internalServerError().build()
}
}
}
@GetMapping("/{id}")
fun findCustomer(@PathVariable("id") id: Long): ResponseEntity<Customer> {
return when (val result = repository.findById(id)) {
is Either.Success -> ResponseEntity.ok(result.entity())
is Either.Failure -> {
when (result.exception()) {
is CustomerNotFound -> ResponseEntity.notFound().build()
else -> ResponseEntity.internalServerError().build()
}
}
}
}
}
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 resulting Either
class will look like this:
sealed class Either<F: Exception, S: Any> {
protected var successHandler: (Success<F, S>) -> ResponseEntity<S> = {
e -> ResponseEntity.ok(e.entity())
}
protected var errorHandler: (Failure<F, S>) -> ResponseEntity<F> = {
e-> ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.exception())
}
data class Success<F: Exception, S: Any>(private val s: S) : Either<F, S>() {
fun entity(): S = s
override fun response(): ResponseEntity<S> = this.successHandler.invoke(this)
}
data class Failure<F: Exception, S: Any>(private val f: F) : Either<F, S>() {
fun exception(): F = f
override fun response(): ResponseEntity<F> = this.errorHandler.invoke(this)
}
abstract fun response(): ResponseEntity<out Any>
fun onFailureDo(action: (e: Failure<F, S>) -> ResponseEntity<F>): Either<F, S> {
this.errorHandler = action
return this
}
fun onSuccessDo(action: (e: Success<F, S>) -> ResponseEntity<S>): Either<F, S> {
this.successHandler = action
return this
}
}
fun <E: Exception> E.failure() = Either.Failure<E, Nothing>(this)
fun <T : Any> T.success() = Either.Success<Nothing, T>(this)
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 fun response(): ResponseEntity<out Any>
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.
data class Success<F: Exception, S: Any>(private val s: S) : Either<F, S>() {
fun entity(): S = s
override fun response(): ResponseEntity<S> = this.successHandler.invoke(this)
}
data class Failure<F: Exception, S: Any>(private val f: F) : Either<F, S>() {
fun exception(): F = f
override fun response(): ResponseEntity<F> = this.errorHandler.invoke(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 var successHandler: (Success<F, S>) -> ResponseEntity<S> = {
e -> ResponseEntity.ok(e.entity())
}
protected var errorHandler: (Failure<F, S>) -> ResponseEntity<F> = {
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:
fun onFailureDo(action: (e: Failure<F, S>) -> ResponseEntity<F>): Either<F, S> {
this.errorHandler = action
return this
}
fun onSuccessDo(action: (e: Success<F, S>) -> ResponseEntity<S>): Either<F, S> {
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
@RequestMapping("/api/customers")
class CustomerController(@Autowired val repository: CustomersRepository) {
@PostMapping
fun createCustomer(@RequestBody customer: Customer, uri: UriComponentsBuilder): ResponseEntity<String> {
return repository.create(customer)
.onSuccessDo { e ->
ResponseEntity
.created(uri
.path("/api/customers/{id})")
.buildAndExpand(e.entity().id)
.toUri()
).build()
}.response() as ResponseEntity<String>
}
@GetMapping("/{id}")
fun findCustomer(@PathVariable("id") id: Long): ResponseEntity<Customer> {
return repository.findById(id)
.onFailureDo { e ->
when (e.exception()) {
is CustomerNotFound -> ResponseEntity.notFound().build()
else -> ResponseEntity.internalServerError().build()
}
}.response() as ResponseEntity<Customer>
}
}
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!