Handle Errors in Reactive Microservices

In this tutorial by Juan Antonio Medina Iglesias, a microservices expert, you’ll learn how to handle and publish errors in reactive microservices.

Any microservice needs to be built for failure, so errors (if any) can be handled effectively and gracefully when you create reactive microservices. The Reactor Framework provides mechanisms that you need to understand to handle such errors. In this tutorial, you’ll learn how to use them to make your reactive microservice efficient at handling errors.

Capturing errors on handlers

When you create handlers, you may encounter errors—and such errors can be handled with one special method in any reactive publisher: onErrorResume. Take a look at the following code snippet to understand how it works:

package com.microservices.chapter4

import org.springframework.http.HttpStatus
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.BodyInserters.fromObject
import org.springframework.web.reactive.function.server.ServerRequest
import org.springframework.web.reactive.function.server.ServerResponse.*
import org.springframework.web.reactive.function.server.bodyToMono
import reactor.core.publisher.onErrorResume
import java.net.URI

@Component
class CustomerHandler(val customerService: CustomerService) {
  fun get(serverRequest: ServerRequest) =
      customerService.getCustomer(serverRequest.pathVariable("id").toInt())
          .flatMap { ok().body(fromObject(it)) }
          .switchIfEmpty(status(HttpStatus.NOT_FOUND).build())

  fun search(serverRequest: ServerRequest) =
      ok().body(customerService.searchCustomers(serverRequest.queryParam("nameFilter")
          .orElse("")), Customer::class.java)

  fun create(serverRequest: ServerRequest) =
      customerService.createCustomer(serverRequest.bodyToMono()).flatMap {
        created(URI.create("/functional/customer/${it.id}")).build()
      }.onErrorResume(Exception::class) {
        badRequest().body(fromObject("error"))
  }
}

Using onErrorResume , you can notify any reactive publisher that if an error is encountered, it can be handled within the method. In the above code snippet, the lambda gives a 400 BAD REQUEST output response with a simple text.

Perform the following JSON request using curl to test this :

curl -X POST \
  http://localhost:8080/functional/customer/ \
  -H 'content-type: application/json' \
  -d '{
	  "id": 18,
	  "name": "New Customer",
	  "telephone": {
		"countryCode": "+44",
		"telephoneNumber": "7123456789"
	  }
	}
bad json'

This request will produce a 400 BAD REQUEST response with a text that says the following:

error

You can create a simple JSON response, bringing the ErrorResponse class:

package com.microservices.chapter4

data class ErrorResponse(val error: String, val message: String)

Now, you can just adapt the response to the error:

package com.microservices.chapter4

import org.springframework.http.HttpStatus
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.BodyInserters.fromObject
import org.springframework.web.reactive.function.server.ServerRequest
import org.springframework.web.reactive.function.server.ServerResponse.*
import org.springframework.web.reactive.function.server.bodyToMono
import reactor.core.publisher.onErrorResume
import java.net.URI

@Component
class CustomerHandler(val customerService: CustomerService) {
  fun get(serverRequest: ServerRequest) =
      customerService.getCustomer(serverRequest.pathVariable("id").toInt())
          .flatMap { ok().body(fromObject(it)) }
          .switchIfEmpty(status(HttpStatus.NOT_FOUND).build())

  fun search(serverRequest: ServerRequest) =
      ok().body(customerService.searchCustomers(serverRequest.queryParam("nameFilter")
          .orElse("")), Customer::class.java)

  fun create(serverRequest: ServerRequest) =
      customerService.createCustomer(serverRequest.bodyToMono()).flatMap {
        created(URI.create("/functional/customer/${it.id}")).build()
      }.onErrorResume(Exception::class) {
        badRequest().body(fromObject(ErrorResponse("error creating customer",
            it.message ?: "error")))
	  }	
}

If you repeat the curl request again, you should get something like this:

{
    "error": "error creating customer",
    "message": "JSON decoding error: Unexpected character ('b' (code 98)): expected a valid value (number, String, array, object, 'true', 'false' or 'null'); nested exception is com.fasterxml.jackson.core.JsonParseException: Unexpected character ('b' (code 98)): expected a valid value (number, String, array, object, 'true', 'false' or 'null')\n at [Source: UNKNOWN; line: 9, column: 2]"
}

Publishing the errors

You know how to handle errors, but sometimes you need to publish them. Try, for example, to create an error if you are trying to create a customer that is already created.

For this, create a simple Exception class, CustomerExistException:

package com.microservices.chapter4

class CustomerExistException(override val message: String) : Exception(message)

You can now modify the create method in the CustomerServiceImpl class to use this new exception:

package com.microservices.chapter4

import com.microservices.chapter4.Customer.Telephone
import org.springframework.stereotype.Component
import reactor.core.publisher.Mono
import reactor.core.publisher.toFlux
import reactor.core.publisher.toMono
import java.util.concurrent.ConcurrentHashMap

@Component
class CustomerServiceImpl : CustomerService {
  companion object {
    val initialCustomers = arrayOf(Customer(1, "Kotlin"),
        Customer(2, "Spring"),
        Customer(3, "Microservice", Telephone("+44", "7123456789")))
  }

  val customers = ConcurrentHashMap<Int, Customer>(initialCustomers.associateBy(Customer::id))

  override fun getCustomer(id: Int) = customers[id]?.toMono() ?: Mono.empty()

  override fun searchCustomers(nameFilter: String) = customers.filter {
		it.value.name.contains(nameFilter, true)
	  }.map(Map.Entry<Int, Customer>::value).toFlux()

	override fun createCustomer(customerMono: Mono<Customer>) =
		customerMono.flatMap {
			if (customers[it.id] == null) {
			  customers[it.id] = it
			  it.toMono()
			} else {
			  Mono.error(CustomerExistException("Customer ${it.id} already 
			 exist"))
			}
		}
}

In this case, you have to check if the customer exists first, and if not, you’ll just store and return it as a Mono. However, if the customer doesn’t exist, you will have to create a Mono.error, which will create a Mono that includes an error.

Now try to send this curl request twice:

curl -X POST \
 http://localhost:8080/functional/customer/ \
 -H 'content-type: application/json' \
 -d '{
 "id": 18,
 "name": "New Customer",
 "telephone": {
 "countryCode": "+44",
 "telephoneNumber": "7123456789"
 }
}
'

The second time that this request is sent, you should get this output with a 400 BAD REQUEST response:

{
    "error": "error creating customer",
    "message": "Customer 18 already exists"
}