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.
There’s a message field, which is self-explanatory.
The overridden function,
getExtensions()
, is to provide a map of parameters. This could be different properties likeErrorCodes
that you want to pass for an error.The overridden function,
getErrorTypes()
, is to provide a classification of the error — for example,DataFetchingException
orValidationException
.
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.