Partner – Microsoft – NPI (cat= Spring)
announcement - icon

Azure Spring Apps is a fully managed service from Microsoft (built in collaboration with VMware), focused on building and deploying Spring Boot applications on Azure Cloud without worrying about Kubernetes.

And, the Enterprise plan comes with some interesting features, such as commercial Spring runtime support, a 99.95% SLA and some deep discounts (up to 47%) when you are ready for production.

>> Learn more and deploy your first Spring Boot app to Azure.

You can also ask questions and leave feedback on the Azure Spring Apps GitHub page.

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 message key 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 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 spring-boot-starter-graphql module, which brings in the required GraphQL dependencies.

We’re also using the spring-graphql-test module for related testing:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-graphql</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.graphql</groupId>
    <artifactId>spring-graphql-test</artifactId>
    <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.

In the following subsections, we’ll see how the Spring Boot module handles exceptions or errors.

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:

{
  "errors": [
    {
      "message": "INTERNAL_ERROR for 2c69042a-e7e6-c0c7-03cf-6026b1bbe559",
      "locations": [
        {
          "line": 2,
          "column": 5
        }
      ],
      "path": [
        "searchByLocation"
      ],
      "extensions": {
        "classification": "INTERNAL_ERROR"
      }
    }
  ],
  "data": null
}

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

By default, any exception during request processing is handled by the ExceptionResolversExceptionHandler class that implements the DataFetcherExceptionHandler interface from the GraphQL API. It allows the application to register one or more DataFetcherExceptionResolver components.

These resolvers are sequentially invoked until one of them is able to handle the exception and resolve it to a GraphQLError. If no resolvers are able to handle the exception then the exception is categorized as an INTERNAL_ERROR. It also contains the execution id and generic error message, as shown above.

5.2. GraphQL Response With Handled Exception

Now let’s see what the response will look like if we implement our custom exception handling.

First, we have another custom exception:

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

DataFetcherExceptionResolver provides an async contract. However, in most cases, it is sufficient to extend DataFetcherExceptionResolverAdapter and override one of its resolveToSingleError or resolveToMultipleErrors methods that resolve exceptions synchronously.

Now, let’s implement this component and we can return a NOT_FOUND classification along with the exception message instead of the generic error:

@Component
public class CustomExceptionResolver extends DataFetcherExceptionResolverAdapter {

    @Override
    protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) {
        if (ex instanceof VehicleNotFoundException) {
            return GraphqlErrorBuilder.newError()
              .errorType(ErrorType.NOT_FOUND)
              .message(ex.getMessage())
              .path(env.getExecutionStepInfo().getPath())
              .location(env.getField().getSourceLocation())
              .build();
        } else {
            return null;
        }
    }
}

Here, we’ve created a GraphQLError with the appropriate classification and other error details to create a more useful response in the errors section of the JSON response:

{
  "errors": [
    {
      "message": "Vehicle with vin: 123 not found.",
      "locations": [
        {
          "line": 2,
          "column": 5
        }
      ],
      "path": [
        "searchByVin"
      ],
      "extensions": {
        "classification": "NOT_FOUND"
      }
    }
  ],
  "data": {
    "searchByVin": null
  }
}

An important detail of this error handling mechanism is that unresolved exceptions are logged at the ERROR level along with the executionId that correlates with the error sent to the client. Any resolved exceptions, as shown above, are logged at DEBUG level in the logs.

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.

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

Course – LS (cat=Spring)

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

>> THE COURSE
res – REST with Spring (eBook) (everywhere)
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.