Error Handling in Spring REST Services With Kotlin | by The Bored Dev | Dec, 2022

Exception handling done right!

photo by taiichi nakamura Feather unsplash

This article will show how to implement exception handling in Spring Web Services. We’ll take you through the options you have in Spring for handling error scenarios and returning a defined response for each.

We’ll also cover a more basic method for error handling without the built-in need for Spring components.

Let’s get started!

Error handling in REST services has always been a bit controversial. This is mainly because there are different approaches that can be taken, each with their own advantages and disadvantages. The fact that there are pros and cons to each of them are in some cases not entirely objective doesn’t help with dealing with these arguments.

We’ll walk you through different ways to handle error scenarios in your Spring Rest services, trying to be as objective as possible. Our goal is for you to decide and decide which approach best suits you.

Before we go into the possible ways to handle exceptions, let’s set the groundwork for our first few examples.

In our first few examples, we’re going to use mock CustomersRepository which 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 make a comeback Customer In either case, what if something doesn’t happen as expected? We will throw an exception and handle it accordingly.

If you need help bootstrapping a simple Spring Boot application to run these examples, you can check out our article “How to Bootstrap a Spring Boot Application,

Let’s get started!

photo by soheb zaidi Feather unsplash

One of the most basic options for handling exceptions in Spring is to use @ExceptionHandler Annotations at the controller level.

How can we do that? let’s see a CustomerController With two basic endpoints. We’ll add a method to handle cases where no customers have 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
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
val result = repository.findById(id)
return ResponseEntity.ok(result)

@ExceptionHandler(CustomerNotFound::class)
fun customerNotFound(exception: CustomerNotFound): ResponseEntity
return ResponseEntity.notFound().build()

In this case, our repository will return CustomerNotFound Exception if the customer does not exist in our database. The exception is first declared like this:

data class CustomerNotFound(override val message: String? = null, val id: Long): Exception(message)

To handle this exception when it is bubbled up by our application, we need to define a handler in our controller. we do it by using @ExceptionHandler annotation.

by using @ExceptionHandler, we map a custom response to the given exception. In this case, the exception given is CustomerNotFound,

There is another way to do something similar. it is using @ResponseStatus annotation.

@ResponseStatus The annotation will map our response to the corresponding status code, so in our example, this would be equivalent to return ResponseEntity.notFound(),

In this case our exception handler method will be slightly different:

@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "Customer could not be found!")
@ExceptionHandler(CustomerNotFound::class)
fun customerNotFound(exception: CustomerNotFound)

Some of the advantages of this approach, in terms of readability or maintainability, are that it is easier to trace how an exception is handled in the current controller and configure response when this exception occurs. It is also a shorthand way of defining exception handling in our code.

On the other hand, using controller-based exception handling doesn’t allow reusing these handlers or even configuring global exception handlers that will apply to all of our controllers.

Let’s now look at another option.

Another way to define exception handlers is @ControllerAdvice annotation. This will allow us to define global exception handling throughout our application. Let’s see how it will look:

@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 client using curl, we’ll also get a 404, just like we saw with controller-based exception handling.

curl -v                                                                                                                                                                                       
* 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 commonly used method of defining exception handling in Spring Boot. Its main advantage is that we do not need to define an exception handler in each controller. We have to define it once in our code, and every controller will reuse it.

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 have to find @ControllerAdvice to find the location where it is configured. Therefore, in terms of readability, this approach is not the best for people who are unfamiliar with the code or Spring.

Another thing we haven’t mentioned before is how Spring handles both of these methods internally. If we look at our log, we can see something like 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 learn a little more about this component.

This component is responsible for handling all exceptions defined by it. @ExceptionHandler annotation. Therefore, it is a very important component in Spring exception handling!

This component also extends beyond any other known component, AbstractHandlerMethodExceptionResolver, it extends from component HandlerExceptionResolver Interface, and it allows us to define our own custom exception resolvers.

This method is not used frequently, as there are better ways to do it now. However, if you still want to see an example of implementing a custom exception resolver, you can find it here Here,

We’ve seen the main methods for handling exceptions provided by Spring, but are there any other ways? We think there is another way which, as always, has its advantages and disadvantages. Let’s see how!

Some views claim that Spring does “too much magic”. Hence, we lose track of what is happening under the covers in our application. We have another proposition for those feeling this way. Let’s get started!

to introduce either for controller-based handling

first we’re going to change our CustomerRepository a return interface Either Monad instead.

We could have used Arrow’s Ether in this example, but we’ve decided to create a custom Either For this example class. it looks like this:

sealed class Either 

data class Success(private val s: S) : Either()
fun entity(): S = s

data class Failure(private val f: F) : Either()
fun exception(): F = f


fun E.failure() = Either.Failure(this)
fun T.success() = Either.Success(this)

As you can see, it’s quite simple. We will see its use with an example.

First, we’re going to modify a custom InMemoryCustomerRepository implementation to use our Either Class.

@Component("inMemoryCostumersRepository")
class InMemoryCustomerRepository(val configuration: DatabaseConfiguration): CustomersRepository
private val customers = mutableMapOf()
override fun create(customer: Customer): Either
val created = customer.copy(id = customers.keys.size.toLong())
customers[created.id!!] = created
return created.success() as Either

override fun findById(id: Long): Either
return ((
customers[id]?.success() ?:
CustomerNotFound("Could not found customer $id", id).failure()
) as Either)

We’ll replace any Spring-based configuration responsible for exception handling and define the responses ourselves. Let’s see how it will look after applying these changes to our controller:

@RestController
@RequestMapping("/api/customers")
class CustomerController(@Autowired val repository: CustomersRepository) {
@PostMapping
fun createCustomer(@RequestBody customer: Customer, uriBuilder: UriComponentsBuilder): ResponseEntity
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
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 advantage of this approach is that we don’t rely on exceptions bubbling. Exception handling is within our code, so we can easily read and understand how it behaves.

As we saw earlier for controller-based exception handling in Spring, this approach has only one drawback, we cannot reuse it for all our controllers at the moment, and we have to define it in each endpoint. Will happen. Can we do anything to solve this? let’s try!

Global exception handling using either

we will try to modify our Either To make the class available to be used for “global” exception handling. The goal is to provide a “default” configuration for our responses, but at the same time, we can override the behavior at the controller level.

which results in Either The class would look like this:

sealed class Either 
protected var successHandler: (Success) -> ResponseEntity =
e -> ResponseEntity.ok(e.entity())

protected var errorHandler: (Failure) -> ResponseEntity =
e-> ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.exception())

data class Success(private val s: S) : Either()
fun entity(): S = s
override fun response(): ResponseEntity = this.successHandler.invoke(this)

data class Failure(private val f: F) : Either()
fun exception(): F = f
override fun response(): ResponseEntity = this.errorHandler.invoke(this)

abstract fun response(): ResponseEntity
fun onFailureDo(action: (e: Failure) -> ResponseEntity): Either
this.errorHandler = action
return this

fun onSuccessDo(action: (e: Success) -> ResponseEntity): Either
this.successHandler = action
return this


fun E.failure() = Either.Failure(this)
fun T.success() = Either.Success(this)

There are a few things to highlight in our new implementation. We will go through 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 the method of our being able to convert Either class for a spring ResponseEntity Thing.

abstract fun response(): ResponseEntity

everyone Either subclass, Success And FailureWill override this method to map ours to the corresponding HTTP response, including the status code and the presence or absence of an entity.

data class Success(private val s: S) : Either() 
fun entity(): S = s
override fun response(): ResponseEntity = this.successHandler.invoke(this)

data class Failure(private val f: F) : Either()
fun exception(): F = f
override fun response(): ResponseEntity = this.errorHandler.invoke(this)

Now you will notice that these two classes call two different handlers instead of returning one ResponseEntity object directly. Why so? Well we decided to do that in order to be able to provide a default response but also allow us to override the default response in our controllers.

Now you must be wondering what is the default configuration?

we initialize both successHandler And errorHandler With our default configuration like this:

protected var successHandler: (Success) -> ResponseEntity = 
e -> ResponseEntity.ok(e.entity())

protected var errorHandler: (Failure) -> ResponseEntity =
e-> ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.exception())

As you can see, any successful response will be converted to a 200 (OK) response, which includes the entity in the body if it exists. On the other hand, any failure will by default be converted to a 500 (Server Error) response, including an exception in the body.

This is just a simple example. It is up to you to decide what you want to include in the body in case of failure. For example, you can include the message that is included in the exception.

Now that we understand how the default behavior is set, how can we override this behavior for a specific case? To allow this, we have provided these two methods:

fun onFailureDo(action: (e: Failure) -> ResponseEntity): Either 
this.errorHandler = action
return this

fun onSuccessDo(action: (e: Success) -> ResponseEntity): Either
this.successHandler = action
return this

You can see how these methods accept a function that receives a Either.Success or one Either.Failure and return a ResponseEntity object with the corresponding failure or success type.

How can we use all this? Come see our new CustomerController After integrating it:

@RestController
@RequestMapping("/api/customers")
class CustomerController(@Autowired val repository: CustomersRepository)
@PostMapping
fun createCustomer(@RequestBody customer: Customer, uri: UriComponentsBuilder): ResponseEntity
return repository.create(customer)
.onSuccessDo e ->
ResponseEntity
.created(uri
.path("/api/customers/id)")
.buildAndExpand(e.entity().id)
.toUri()
).build()
.response() as ResponseEntity

@GetMapping("/id")
fun findCustomer(@PathVariable("id") id: Long): ResponseEntity
return repository.findById(id)
.onFailureDo e ->
when (e.exception())
is CustomerNotFound -> ResponseEntity.notFound().build()
else -> ResponseEntity.internalServerError().build()

.response() as ResponseEntity

You can see how our controller now uses the default configuration for failover createCustomer method and success findCustomer way. For example, findCustomer Will then return a 200 (OK) response and include the client in the JSON body.

However, we override the behavior for success createCustomer method and for failure to findCustomer way. Why so? In our case, successfully creating a customer means returning 201 (CREATED) with a Location Header, as we have seen earlier in this article. We also need a different behavior for failures in findCustomer to be able to handle CustomerNotFound thoroughly.

That’s pretty cool, isn’t it? 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 be able to easily read and understand how exception handling is configured in our code that includes any Spring” Magic” is not included!

image by Aziz Achari on Unsplash

In this article, we have seen different ways of handling exceptions in Spring Boot application. As we said earlier, exception handling is a very controversial topic, as there are different opinions about which approach works best in our applications. Hopefully, we have been able to bring an objective and unbiased view of the subject, and now you can use it to make your own assumptions and make the right decisions in your projects.

That’s all from us today! We really hope you enjoy this article as much as we enjoyed writing it!

Looking forward to seeing you with us again soon!

Thanks for reading!

Leave a Reply