Error Handling With GraphQL, Spring Boot, and Kotlin

Error Handling With GraphQL, Spring Boot, and Kotlin

Modeling GraphQL errors and exceptions using Spring Boot with Kotlin

Problem

Error handling in GraphQL can be pretty tricky, as a response will always have HTTP status 200 OK. If a request fails, the JSON payload response will contain a root field called errors that contains the details of the failure.

For a query like the one below …

query{
  getCar(input: "B Class"){
    name
  }
}

… without proper error handling, the response will be something like this:

{
  "errors": [
    {
      "message": "Internal Server Error(s) while executing query"
    }
  ],
  "data": null
}

This isn’t helpful at all — the response doesn’t tell you what actually went wrong.

Solution

1. The setup

Let’s set up a Spring Boot project with GraphQL. Also, the code to this is available here.

Go to https://start.spring.io/.

Select a Gradle project, and set the language as Kotlin.

You don’t have to select any dependencies for a bare-minimum implementation.

Download the ZIP file, and open it in your favorite IDE.

Jump to build.gradle.kts, and add:

implementation("com.graphql-java-kickstart:graphql-spring-boot-starter:6.0.1")
implementation("com.graphql-java-kickstart:graphql-java-tools:5.7.1")
runtimeOnly("com.graphql-java-kickstart:altair-spring-boot-starter:6.0.1")
runtimeOnly("com.graphql-java-kickstart:graphiql-spring-boot-starter:6.0.1")
runtimeOnly("com.graphql-java-kickstart:voyager-spring-boot-starter:6.0.1")
runtimeOnly("com.graphql-java-kickstart:playground-spring-boot-starter:6.0.1")

These are the GraphQL libraries you’d need to get started with your first endpoint.

2. Endpoint without error handling

Let’s create two endpoints to create and fetch a resource.

type Query{
    hello: String
    getCar(input: String) : Car!
}

type Mutation{
    createCar(input: CarInput): Car
}

type Car {
    name: String
    price: Int
    engineType: String
}

input CarInput{
    name: String!
    price: Int!
    engineType: String!
}

Add Service to provide the creation and fetching of car.

@Service
class CarService {
    fun getCar(input: String): Car {
        return CarMap.cars[input]!!
    }

    fun createCar(input: CarInput): Car {
        val car = Car(input.name, input.price, input.engineType)
        CarMap.cars[input.name] = car
        return car
    }
}

object CarMap {
    val cars = HashMap<String, Car>()
}

We have created a function to create a car and another to get back the car with the name as input. But in case the car isn’t found, this will throw an exception, and that gets translated to the response shown:

{
  "errors": [
    {
      "message": "Internal Server Error(s) while executing query"
    }
  ],
  "data": null
}

3. Implement error handling

To be able to provide something sensible in case of an error or exception, we’ll add some more code.

open class CarNotFoundException(
        errorMessage: String? = "",
        private val parameters: Map<String, Any>? = mutableMapOf()
) : GraphQLException(errorMessage) {
    override val message: String?
        get() = super.message

    override fun getExtensions(): MutableMap<String, Any> {
        return mutableMapOf("parameters" to (parameters ?: mutableMapOf()))
    }

    override fun getErrorType(): ErrorClassification {
        return ErrorType.DataFetchingException
    }
}

Let’s understand what’s going on here.

  1. There’s a message field, which is self-explanatory.

  2. The overridden function, getExtensions(), is to provide a map of parameters. This could be different properties like ErrorCodes that you want to pass for an error.

  3. The overridden function, getErrorTypes(), is to provide a classification of the error — for example, DataFetchingException or ValidationException.

In the above exception, you can see it extends from GraphQLException, which is the bridge between our custom CarNotFoundException and GraphQLError.

Now we’re going to add GraphQLException.

When we add that, we need to make sure we create a separate Java module and add that GraphQLException in the Java module.

Why?

Because a Kotlin class can’t extend RuntimeException and implement GraphQLError at the same time. The reason for that can be found here.

So now we create a GraphQLException class.


public class GraphQLException extends RuntimeException implements GraphQLError {

    String customMessage;

    public GraphQLException(String customMessage) {
        this.customMessage = customMessage;
    }

    @Override
    public String getMessage() {
        return customMessage;
    }

    @Override
    public List<SourceLocation> getLocations() {
        return null;
    }

    @Override
    public ErrorClassification getErrorType() {
        return null;
    }
}

And from the CarService, we throw the exception when the car isn’t found.

@Service
class CarService {
    fun getCar(input: String): Car {
        return CarMap.cars[input]
                ?: throw CarNotFoundException("Car with name = $input not found",
                        mapOf("name" to input))
    }

    fun createCar(input: CarInput): Car {
        val car = Car(input.name, input.price, input.engineType)
        CarMap.cars[input.name] = car
        return car
    }
}

With that change, the response will contain a lot more detail than before.

{
  "errors": [
    {
      "message": 
      "Exception while fetching data (/getCar) : Car with name = B Class not found",

      "path": [
        "getCar"
      ],
      "extensions": {
        "parameters": {
          "name": "B Class"
        },
        "classification": "DataFetchingException"
      }
    }
  ],
  "data": null
}

Conclusion

You can find the full implementation here.

I’m glad you made it to the end. It means you’re interested in GraphQL.