Spring Top

Get started with Spring 5 and Spring Boot 2, through the reference Learn Spring course:

>> LEARN SPRING

1. Overview

In this tutorial, we'll learn about the error handling options in GraphQL. We'll look at what the GraphQL spec says about the error responses. Consequently, we'll develop an example of GraphQL error handling using Spring Boot.

2. Response Per GraphQL Specification

As per the GraphQL specification, every request received must return a well-formed response. This well-formed response consists of the map of data or errors from the respective successful or unsuccessful requested operation. Additionally, a response may contain partial successful result data and field errors.

The key components of the response map are errors, data, and extensions.

The errors section in the response describes any failure during the requested operation. If no error occurs, the errors component must not be present in the response. In the next section, we'll look into the different kinds of errors described in the specification.

The data section describes the result of the successful execution of the requested operation. If the operation is a query, this component is an object of query root operation type. On the other hand, if the operation is a mutation, this component is an object of the mutation root operation type.

If the requested operation fails even before the execution due to missing information, validation errors, or syntax errors, then the data component must not be present in the response. And if the operation fails during the execution of the operation with an unsuccessful result, then the data component must be null.

The response map may contain an additional component called extensions, which is a map object. The component facilitates the implementors to provide other custom contents in the response as they see fit. Hence, there are no additional restrictions on its content format.

If the data component isn't present in the response, then the errors component must be present and must contain at least one error. Further, it should indicate the reasons for the failures.

Here's an example of a GraphQL error:

mutation {
  addVehicle(vin: "NDXT155NDFTV59834", year: 2021, make: "Toyota", model: "Camry", trim: "XLE",
             location: {zipcode: "75024", city: "Dallas", state: "TX"}) {
    vin
    year
    make
    model
    trim
  }
}

The error response when a unique constraint is violated will look like:

{
  "data": null,
  "errors": [
    {
      "errorType": "DataFetchingException",
      "locations": [
        {
          "line": 2,
          "column": 5,
          "sourceName": null
        }
      ],
      "message": "Failed to add vehicle. Vehicle with vin NDXT155NDFTV59834 already present.",
      "path": [
        "addVehicle"
      ],
      "extensions": {
        "vin": "NDXT155NDFTV59834"
      }
    }
  ]
}

3. Errors Response Component per GraphQL Specification

The errors section in the response is a non-empty list of errors, each of which is a map.

3.1. Request Errors

As the name suggests, request errors may occur before the operation execution if there is any issue with the request itself. It may be due to request data parsing failure, request document validation, an unsupported operation, or invalid request values.

When a request error occurs, this indicates that execution has not begun, which means the data section in the response must not be present in the response. In other words, the response contains only the errors section.

Let's see an example demonstrating the case of invalid input syntax:

query {
  searchByVin(vin: "error) {
    vin
    year
    make
    model
    trim
  }
}

Here's the request error response for a syntax error, which in this case was a missing quote mark:

{
  "data": null,
  "errors": [
    {
      "message": "Invalid Syntax",
      "locations": [
        {
          "line": 5,
          "column": 8,
          "sourceName": null
        }
      ],
      "errorType": "InvalidSyntax",
      "path": null,
      "extensions": null
    }
  ]
}

3.2. Field Errors

Field errors, as the name suggests, may occur due to either failure to coerce the value into the expected type or an internal error during the value resolution of a particular field. It means that field errors occur during the execution of the requested operation.

In case of field errors, the execution of the requested operation continues and returns a partial result, meaning the data section of the response must be present along with all the field errors in the errors section.

Let's look at another example:

query {
  searchAll {
    vin
    year
    make
    model
    trim
  }
}

This time, we've included the vehicle trim field, which is supposed to be non-nullable according to our GraphQL schema.

However, one of the vehicles' information has a null trim value, so we're getting back only partial data – the vehicles whose trim value is not null – along with the error:

{
  "data": {
    "searchAll": [
      null,
      {
        "vin": "JTKKU4B41C1023346",
        "year": 2012,
        "make": "Toyota",
        "model": "Scion",
        "trim": "Xd"
      },
      {
        "vin": "1G1JC1444PZ215071",
        "year": 2000,
        "make": "Chevrolet",
        "model": "CAVALIER VL",
        "trim": "RS"
      }
    ]
  },
  "errors": [
    {
      "message": "Cannot return null for non-nullable type: 'String' within parent 'Vehicle' (/searchAll[0]/trim)",
      "path": [
        "searchAll",
        0,
        "trim"
      ],
      "errorType": "DataFetchingException",
      "locations": null,
      "extensions": null
    }
  ]
}

3.3. Error Response Format

As we saw earlier, errors in the response are a collection of one or more errors. And, every error must contain a key message that describes the failure reasons so the client developer can make necessary corrections to avoid the error.

Each error may also contain a key called locations, which is a list of locations pointing to a line in the requested GraphQL document associated with an error. Each location is a map with keys: line and column, respectively, providing the line number and beginning column number of the associated element.

The other key that may be the part of an error is called path. It provides the list of values from the root element traced to the particular element of the response that has the error. A path value can be a string representing the field name or index of the error element if the field value is a list. If the error is related to a field with an alias name, then the value in the path should be the alias name.

3.4. Handling Field Errors

Whether a field error is raised on a nullable or non-nullable field, we should handle it as if the field returned null and the error must be added to the errors list.

In the case of a nullable field, the field's value in the response will be null but errors must contain this field error describing the failure reasons and other information, as seen in the earlier section.

On the other hand, the parent field handles the non-nullable field error. If the parent field is non-nullable, then the error handling is propagated until we reach a nullable parent field or the root element.

Similarly, if a list field contains a non-nullable type and one or more list elements return null, the whole list resolves to null. Additionally, if the parent field containing the list field is non-nullable, then the error handling is propagated until we reach a nullable parent or the root element.

For any reason, if multiple errors are raised for the same field during resolution, then for that field, we must add only one field error into errors.

4. Spring Boot GraphQL Libraries

Our Spring Boot application example uses the graphql-spring-boot-starter module, which brings in graphql-java-servlet and graphql-java.

We're also using the graphql-java-tools module, which helps map a GraphQL schema to existing Java objects, and for the unit tests, we're using graphql-spring-boot-starter-test:

<dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphql-spring-boot-starter</artifactId>
    <version>5.0.2</version>
</dependency>

<dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphql-java-tools</artifactId>
    <version>5.2.4</version>
</dependency>

And for the tests, we use graphql-spring-boot-starter-test:

<dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphql-spring-boot-starter-test</artifactId>
    <version>5.0.2</version>
    <scope>test</scope>
</dependency>

5. Spring Boot GraphQL Error Handling

In this section, we'll mainly cover GraphQL error handling in the Spring Boot application itself. We won't cover the GraphQL Java and GraphQL Spring Boot application development.

In our Spring Boot application example, we'll mutate or query for vehicles based on either location or VIN (Vehicle Identification Number). We'll see different ways to implement error handling using this example.

The graphql-java-servlet module provides an interface called GraphQLErrorHandler. We can provide our implementation of it.

In the following subsections, we'll see how the graphql-java-servlet module handles exceptions or errors, using components from the graphql-java module.

5.1. GraphQL Response With Standard Exception

Generally, in a REST application, we create a custom runtime exception class by extending RuntimeException or Throwable:

public class InvalidInputException extends RuntimeException {
    public InvalidInputException(String message) {
        super(message);
    }
}

With this approach, we can see the GraphQL engine returns the following response:

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

In the above error response, we can see that it doesn't contain any details of the error.

By default, any custom exception is handled by the SimpleDataFetcherExceptionHandler class. It wraps the original exception along with source location and execution path, if present, into another exception called ExceptionWhileDataFetching. Then it adds the error to the errors collection. The ExceptionWhileDataFetching, in turn, implements the GraphQLError interface.

After SimpleDataFetcherExceptionHandler handler, another handler called DefaultGraphQLErrorHandler processes the errors collection. It segregates all exceptions of type GraphQLError as client errors. But on top of that, it also creates a GenericGraphQLError exception for all the other non-client errors, if present.

In the above example, InvalidInputException isn't a client error because it only extends RuntimeException and doesn't implement GraphQLError. Consequently, the DefaultGraphQLErrorHandler handler creates a GenericGraphQLError exception representing the InvalidInputException with an internal server error message.

5.2. GraphQL Response With Exception of Type GraphQLError

Now let's see what the response will look like if we implement our custom exception as GraphQLError. The GraphQLError is an interface that allows us to provide more information about the errors by implementing the getExtensions() method.

Let's implement our custom exceptions:

public class AbstractGraphQLException extends RuntimeException implements GraphQLError {
    private Map<String, Object> parameters = new HashMap();

    public AbstractGraphQLException(String message) {
        super(message);
    }

    public AbstractGraphQLException(String message, Map<String, Object> additionParams) {
        this(message);
        if (additionParams != null) {
            parameters = additionParams;
        }
    }

    @Override
    public String getMessage() {
        return super.getMessage();
    }

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

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

    @Override
    public Map<String, Object> getExtensions() {
        return this.parameters;
    }
}
public class VehicleAlreadyPresentException extends AbstractGraphQLException {

     public VehicleAlreadyPresentException(String message) {
         super(message);
     }

    public VehicleAlreadyPresentException(String message, Map<String, Object> additionParams) {
        super(message, additionParams);
    }
}

As we can see in the above code snippet, we have returned null for getLocations() and getErrorType() methods because the default wrapper exception, ExceptionWhileDataFetching, only invokes the getMesssage() and getExtensions() methods of our custom wrapped exception.

As we saw in the earlier section, SimpleDataFetcherExceptionHandler class handles the data fetching error. Let's look at how the graphql-java library helps us in setting the path, locations, and error type.

The below code snippet shows that the GraphQL engine execution uses DataFetcherExceptionHandlerParameters class to set the error field location and path. And these values are passed as constructor arguments to ExceptionWhileDataFetching:

...
public void accept(DataFetcherExceptionHandlerParameters handlerParameters) {
        Throwable exception = handlerParameters.getException();
        SourceLocation sourceLocation = handlerParameters.getField().getSourceLocation();
        ExecutionPath path = handlerParameters.getPath();

        ExceptionWhileDataFetching error = new ExceptionWhileDataFetching(path, exception, sourceLocation);
        handlerParameters.getExecutionContext().addError(error);
        log.warn(error.getMessage(), exception);
}
...

Let's look at a snippet from the ExceptionWhileDataFetching class. Here, we can see that the error type is DataFetchingException:

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

@Override
public List<Object> getPath() {
    return path;
}

@Override
public ErrorType getErrorType() {
    return ErrorType.DataFetchingException;
}
...

6. Conclusion

In this tutorial, we learned different types of GraphQL errors. We also looked at how to format the GraphQL errors per the specification. Later we implemented error handling in a Spring Boot application.

Please note that the Spring team, in collaboration with the GraphQL Java team, is developing a new library, spring-boot-starter-graphql, for Spring Boot with GraphQL. It's still in the milestone release phase and not a general availability (GA) release yet.

As always, the complete source code is available over on GitHub.

Spring bottom

Get started with Spring 5 and Spring Boot 2, through the Learn Spring course:

>> THE COURSE
Generic footer banner
guest
0 Comments
Inline Feedbacks
View all comments