Understanding GraphQL Error Handling Mechanisms in Spring-Boot
This article is the second part of the series GraphQL, from Theory to real-world with Spring boot.
Today’s episode is about setting up your Spring-boot project to host your first GraphQL API entry points then call them using the web-based tool GraphiQL.
It also aims to bring an in-depth understanding of how the Graphql Java Implementation(graphql-java) deals with error handling and especially how to rely on that, by using the GraphQL Spring Framework boot starter, to handle efficiently errors within our app, as this is an aspect that is often overlooked by early graphql adopters.
Check the corresponding video
Before we start, let’s introduce some context.
GraphQL quick summary
GraphQL is a specification that defines not only a new query language for APIs but also how data is returned in response to these queries.
It is an alternative to using Restful APIs in the sense that it solves some of their drawbacks with new features such as these described ones:
- The client has full control over the data returned to him, he receives only what he needs, no more, no less. No unnecessary data is therefore loaded onto the network. No over-etching.
- The client can request several resources at once in a single request, no need to send multiple requests. Ideal for slow mobile connections.
- The server defines what is possible or not with the notion of types, to force the client to request only what is possible.
With great power comes great responsibilities
According to the features mentioned above, the client must be highly considered when setting up such an API. It is more than important to return a response format that is consistent and predictable across several scenarios, to make it easier for the customer to consume the response.
According to the specification, a GraphQL response should follow this pattern:
A GraphQL response might always have the HTTP status 200 OK.
HTTP error codes are not relevant when using GraphQL because,
If a request fails, the JSON payload of the server’s response will contain a root field called “errors” that contains precise information about the problems that occurred on the server-side.
Moreover, since GraphQL allows for multiple operations to be sent in the same request, it’s well possible that a request only partially fails and returns actual data and errors.
The response body could contain the following fields:
“data”: where the data resulting from the operation reside.
“errors”: where all the errors are filled in.
“extensions”: for additional information about the response
The “errors” field, should contain the following fields :
“message”: a key describing the error
“locations”: a list of graph coordinates where the errors have occurred.
“path”: a key describing the absolute path from the root of the graph to the field where an error has occurred.
“extensions” key for the metadata related to the errors.
You can learn more about GraphQL response format by referring to the specification part that deals with this topic.
As far as we are concerned, we will just focus on the “errors” key.
By relying on the graphql-java and graphql-spring-boot within a spring-boot application, we will dissect the underlying mechanism of errors handling within these libraries to let you know what are your possibilities when it comes to customizing some of the default error handling mechanisms.
Application Set up
We will use a simple spring-boot
sample-graphql-error-handling that will expose two endpoints:
createUser(username:string, password:string) that fails if a user is already registered with the same username.
getUser(username:string,password:string) that fails if no user is existing for the given username.
Generate the project using Spring Initializr and add the following dependencies to the pom.xml file:
Our User model Class: User.java
The Schema definition file:
The schema speaks for itself, we will have a read request
getUser which will return a
User corresponding to the username and password given as parameters, and a write request which will register a
User with its credentials as parameters.
We will use a service class, acting as our business logic that will maintain a list of in-memory users. This service will be injected into our resolvers later.
createUser method will return a
UserAlreadyExistsException if a user already exists. Similarly, the
getUsermethod will return a
UserNotFoundException in case no user is found.
Next, we have the code of our exceptions:
We will rely on these customized exceptions to return relevant information to the customer.
Finally, the resolvers code:
Now that the project is set up, we can start sending the requests and analyze the errors. Run the spring boot app and launch the graphiql tool within the browser with:
localhost:8080/graphiqlif you are running on the local machine with the default spring boot app port.
The generic error (case#1)
Within the graphiql tool, let’s start by creating a user:
Everything is OK and the user is indeed created as confirmed by the answer:
If we try to get a non-existing user:
The request results in the following error:
Not only are the details as defined by the specification missing, but the error message is also inconsistent, as it indicates a server-side error yet it is a client-side error.
Also, if we try to create a user that is already existing:
We get the exact same error response.
Let’s consider what just happened as CASE #1
For CASE #2: We need to customize the information according to the error that occurred while giving the client additional information about what happened in the backend so that he can react accordingly.
Customizing errors (case#2)
We’ve already taken a step forward by defining and throwing problem-related customized exceptions. However, these exceptions need to be transformed into GraphQL errors that respect the standard format before being sent to the client.
graphql-java provides the
GraphQLError interface representing a GraphQL standard error that our exceptions will implement.
Our exceptions become:
getErrorType() is mandatory.
We will just return
nullin these methods because they are ignored by the default error handler anyway. We will see why later.
In the case of wrong identification data, we want to add an “invalidField” field under the key “extensions” of the error to indicate to the customer which field is in error and needs to be corrected. This is why we redefine the
Don’t forget to change the instantiation of the exception in
throw new UserNotFoundException(“We were unable to find a user with the provided credentials”, “username”);
Let’s start the application again and run the same test scenario with graphiql to compare the results.
Let’s start by creating a user, the answer is, unsurprisingly, this one:
Then, if you try to retrieve a user that doesn’t exist, the request results in the following error:
Note the invalid field present in the extensions block of the result.
Also, If we try to create a user with an existing username, the response is as follows:
Let’s dissect what just happened in both cases #1 and #2:
When we throw an exception while fetching data:
- The exception is handled by default by the
The handler wraps 4 things: the thrown custom exception (the RuntimeException), the exception message, the error locations, and the extensions, within an
ExceptionWhileDataFetchingerror (which is an implementation of the
ExceptionWhileDataFetchingis added to the list of errors of the query result. As we can see in the following SimpleDataFetcherExceptionHandler code.
At line 10, a new
ExceptionWhileDataFetchingis constructed with the error path, our exception, and the sourceLocation.
Remember I told you the methods
getErrorType() of our exception were ignored?
Well here’s why:
In this code snippet from the ExceptionWhileDataFetching class, only the methods
getExtensions() are called, the path and the sourceLocation are already provided upon class instantiation.
2. The DefaultGraphQLErrorHandler
SimpleDataFetcherExceptionHandler process, another handler, defined by the graphql-spring-boot library comes into action to handle the returned list of errors.
He is the
GraphQLErrorHandler: the default implementation is the
These 2 steps can be illustrated as follows:
In case#1: Our custom exceptions were not instances of GraphQLErrors, the
DefaultGraphQLErrorHandler considered them as internal server errors, containing details that should not reach the client. They were filtered and converted to
GenericGraphQLError as illustrated.
In case#2: Our custom exceptions were instances of GraphQLErrors, hence they were not reduced to Generic graphQL errors.
This behavior is described in the
Now we clearly understand why we get such a useful error response and a valid format for case#2.
SimpleDataFetcherExceptionHandler behavior doesn’t fit your use case, you can create a
DataFectcherExceptionHandler, then define your own logic and throw something different than an
However, this is not something I will advise as you can find yourself breaking the rules of the GraphQL Specification by sending non-standard error messages. Unless you really understand what you are trying to implement.
Besides, customizing this behavior implies modifying the query execution strategy. As graphql-spring-boot integration is relying on graphql-java to implement the query execution strategy, you will have to deal with them, which, IMHO, does not worth it, as they already implement a pretty good behavior.
Similarly, If the
DefaultGraphQLErrorHandlerbehavior doesn’t fit your use case, you can create a
GraphQLErrorHandler, then define your own logic and handle GraphQLErrors differently.
Let’s supposed we are not satisfied by the string:
Exception while fetching data (/xxxx) which is automatically appended to the error message sent to the user, and we just want our original message in the error, without any fancies. We will send our unwrapped original custom exception to the final user by defining a
Sending unwrapped exception
We create the CustomGraphErrorHandler as follows:
We override the
processErrorsmethod by telling the handler to: first check if the exception is an
ExceptionWhileDataFetching then extract our original exception(
UserExistsException …) and return it. Otherwise, the handler will just return the received exception as it is.
If we try to find a non-existing user, we get the following error:
As our exceptions are sent back to the user unwrapped, the message is now concise and precise.
In this article, we were focusing two notions:
Setting up your Spring-boot project to host your first GraphQL API entry points,
Understanding how the graphql-java implementation and graphql-spring-boot handle errors triggered within an application.
We have covered the following sections:
- Setting up a sample project
- Understanding the standard Graphql error response format
- Diving into the mechanisms behind generic graphql errors and more tailored ones
- Adding additional custom information to the error
- Getting to know the possibilities of further error customizations.
The Github repo about this article is available here.
In our next article, we will see how to Secure our APIs with pretty simple steps.
Thanks for reading, feedback is a gift 🎁.