GraphQL APIs differ significantly from REST APIs when it comes to error handling. While partial responses are not possible in REST APIs, GraphQL APIs are designed to allow partial responses in the event of an error condition. This presents some interesting challenges and design considerations that developers and architects should know.
In this article, we will explore the error-handling capabilities of GraphQL. We will examine the GraphQL response structure and how it helps us effectively convey errors. Additionally, we will examine the HTTP status codes returned by the GraphQL service, mainly in the context of Spring for GraphQL. Finally, we will see code examples of handling errors by customizing the errors returned by Spring for GraphQL.
If you’re interested in learning more about GraphQL, be sure to check out our other posts.
GraphQL Response Format
When a GraphQL API is queried, the response is a JSON object that follows a specific format. This format includes three main components: data, errors, and extensions. The “data” contains the requested data, while the “errors” contains a list of any errors encountered during the query. The “extensions” is an optional field that can include additional information about the query, such as performance metrics, tracing, etc.
{
"data": <data>,
"errors": <list of GraphQL errors>,
"extensions": <map of extensions>
}
HTTP Status Codes
The GraphQL specification does not require a specific transport protocol for communication between the client and server. However, HTTP is the most commonly used transport protocol in practice, and JSON is the most popular serialization choice for GraphQL APIs. Additionally, every GraphQL request is an HTTP POST request.
In Spring for GraphQL, the transport protocols are managed by Spring (either Spring MVC or Webflux). The underlying GraphQL Java library does not impose any specific transport protocol.
During normal execution, the server always returns an HTTP status code of 200 OK, even in the case of an error. However, if there are errors, the response body contains an “errors” field in JSON response body. This is an important distinction from a REST API, as it allows the server to provide a partial response with errors, explaining why some data could not be retrieved.
In certain cases, a GraphQL request may be rejected before being processed by the GraphQL engine. In these instances, the HTTP status code can indicate the issue with the request. For instance, a 401 Unauthorized code is returned for authentication failures, while a 400 Bad Request is returned for invalid GraphQL requests.
Partial Response
Let’s understand how errors are returned by the GraphQL service using the following GraphQL schema as a reference.
type Query {
bookById(id : ID) : Book
}
type Book {
id : ID
name : String
author: String
price: Float
ratings: [Rating]
}
type Rating {
id: ID
rating: Int
comment: String
user: String
}
A client can request a book by its ID as follows:
query GetBookByID {
bookById(id: 1) {
id
name
author
ratings {
id
rating
comment
}
}
}
Under normal circumstances, the server’s response can look like:
{
"data": {
"bookById": {
"id": "1",
"name": "Zero to One",
"author": "Peter Thiel",
"ratings": [
{
"id": "1",
"rating": 5,
"comment": "The 4 minutes that will help you decide if this book is for you"
},
{
"id": "2",
"rating": 3,
"comment": "Is Peter Thiel the next robber baron?"
},
{
"id": "3",
"rating": 3,
"comment": "Simple-minded. Is it satire? Poorly-reasoned? Historically-ignorant? Afraid of competition? IDK"
}
]
}
}
}
Above is an HTTP POST request, and the server returned a status code of 200.
If we simulate an error condition, such as throwing a RuntimeException
, the server will return an error.
@QueryMapping
public Book bookById(@Argument Integer id) {
throw new RuntimeException("Something went wrong");
}
The response returned by the server can be in the following format:
{
"errors": [
{
"message": "INTERNAL_ERROR for 0b4ea8f6-1",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"bookById"
],
"extensions": {
"classification": "INTERNAL_ERROR"
}
}
],
"data": {
"bookById": null
}
}
Notice that data and errors are present in the response, even though the data is null
.
This becomes more interesting when the child data fetcher throws an exception.
@QueryMapping
public Book bookById(@Argument Integer id) {
return bookCatalogService.bookById(id);
}
@SchemaMapping
public List<Rating> ratings(Book book) {
throw new RuntimeException("Something went wrong");
// return bookCatalogService.ratings(book);
}
The response returned by the server in this case is:
{
"errors": [
{
"message": "INTERNAL_ERROR for bdb240fb-4",
"locations": [
{
"line": 6,
"column": 5
}
],
"path": [
"bookById",
"ratings"
],
"extensions": {
"classification": "INTERNAL_ERROR"
}
}
],
"data": {
"bookById": {
"id": "1",
"name": "Zero to One",
"author": "Peter Thiel",
"ratings": null
}
}
}
As you can see, the server returns a partial response, with the ratings
field as null
and not-so-useful error messages.
In GraphQL, every “errors
” field must contain a “message
” field that describes the error. The “locations
” field is linked to the GraphQL request document to make it easier for developers to find the cause of the error.
The GraphQL specification also allows for an optional key extensions
, which can be a map of additional data. The GraphQL specification doesn’t enforce any restrictions on the map’s content. In the preceding example, the error classification is INTERNAL_ERROR
.
GraphQL Errors
The errors in GraphQL can be classified into two categories: request errors and field errors.
Request Errors
The request error occurs when there is an issue with the request itself, such as invalid GraphQL syntax.
In this case, the GraphQL response will contain the errors
key but not the data
key.
Field Errors
A field error occurs when an error occurs during the execution of a particular field, resulting in a partial response. This error may occur due to an internal error during the execution of DataFetcher
.
Field errors are typically the fault of the GraphQL service.
Due to partial response, a client cannot rely solely on the null
value. It must always inspect the errors field to determine the error condition.
Error Classification
GraphQL Java provides classifications for commonly occurring errors in the class ErrorType
.
Classification | Description |
---|---|
InvalidSyntax | invalid GraphQL syntax |
ValidationError | Request error due to invalid request |
DataFetchingException | Field error raised during data fetching |
NullValueInNonNullableField | Field error when a field is defined as non-null in the schema |
returns a null value | |
OperationNotSupported | Request error if a request attempts to perform an operation not defined in the schema |
ExecutionAborted | Indicates that the current execution should be aborted |
Spring for GraphQL adds a few more error classifications, such as:
- BAD_REQUEST
- UNAUTHORIZED
- FORBIDDEN
- NOT_FOUND
- INTERNAL_ERROR
If any exception is unresolved, then it will be classified by default as INTERNAL_ERROR
.
Custom Error Handling
Spring for GraphQL enables you to include custom error classifications if the provided classification is insufficient for your use case. Additionally, you can add custom metadata, such as the error code, in the extensions field. This provides a standardized way of communicating problems between server and client applications.
Let’s try to understand this through an example!
A popular approach to handling application errors is throwing a domain-specific exception and letting the appropriate layer handle it. For example, when asked for a particular ID, the repository implementation may throw ResourceNotFoundException
if the requested ID doesn’t exist.
public Book findBookById(Integer bookId) {
var book = bookStorage.get(bookId);
if (book == null) {
throw new ResourceNotFoundException("Book not found", Map.of("ID", String.valueOf(bookId)));
}
return book;
}
The error response returned to the client in this case is:
{
"errors": [
{
"message": "INTERNAL_ERROR for dd08cb12-24ff-0856-2c04-2e342b0ab19e",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"bookById"
],
"extensions": {
"classification": "INTERNAL_ERROR"
}
}
],
"data": {
"bookById": null
}
}
In the above error response, Spring for GraphQL includes a generic message to prevent the leakage of implementation details to the client.
To customize the error response, we can implement an exception resolver as follows:
@Component
public class CustomErrorMessageResolver extends DataFetcherExceptionResolverAdapter {
@Override
protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) {
return GraphqlErrorBuilder.newError(env)
.errorType(ErrorType.INTERNAL_ERROR)
.message("Not Found")
.build();
}
}
In this code, we are extending DataFetcherExceptionResolverAdapter
and overriding the resolveToSingleError
method in the CustomErrorMessageResolver
class. Then, we build a new error using GraphqlErrorBuilder
and add the error type and message details.
The server returns the following response:
{
"errors": [
{
"message": "Not Found",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"bookById"
],
"extensions": {
"classification": "NOT_FOUND"
}
}
],
"data": {
"bookById": null
}
}
This is much better. However, what if you want to include custom classification and error codes in the response?
To achieve this, you can create custom error types and include error codes in the extensions
field like this:
protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) {
if (ex instanceof ResourceNotFoundException resourceNotFoundException) {
var errorCode = resourceNotFoundException.getErrorCode();
var message = resourceNotFoundException.getMessage();
return GraphqlErrorBuilder.newError(env)
.errorType(CustomErrorType.RESOURCE_NOT_FOUND)
.message(message)
.extensions(Map.of("errorCode", errorCode.getShortCode()))
.build();
}
return null;
}
The syntax
ex instanceof ResourceNotFoundException resourceNotFoundException
is Pattern Matching for instanceof
Where CustomErrorType
is defined as:
public enum CustomErrorType implements ErrorClassification {
RESOURCE_NOT_FOUND,
INVALID_OPERATION;
}
The server’s response in this case is:
{
"errors": [
{
"message": "Book not found",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"bookById"
],
"extensions": {
"errorCode": "ResourceNotFound",
"classification": "RESOURCE_NOT_FOUND"
}
}
],
"data": {
"bookById": null
}
}
Customizing error responses allows you to establish a standard contract with the client application. This hides implementation details by returning useful error messages and codes.
Code
The working code example for this article is available on GitHub. To run the example, clone the repository and import graphql-spring-error
as a Gradle project in your favorite IDE.
You can run the application from your IDE, which will start the GraphQL server on port 8080. To test the application, you can use GraphiQL.
Summary
The response of a GraphQL API includes three main fields: “data”, “errors”, and “extensions”. The “data” field contains the requested data, while the “errors” field contains a list of any errors encountered during the query. The optional “extensions” field can include additional information about the query.
HTTP status codes indicate issues with the request, and partial responses may be returned with errors to explain why certain data could not be retrieved. In GraphQL, errors can fall under two categories: request errors or field errors. Implementing custom error handling is recommended to provide clients with uniform error messages and codes.