Handling GraphQL Errors with Hasura & React

This tutorial was written and published as part of the Hasura Technical Writer Program

Table of Contents

Introduction

Unlike REST APIs, GraphQL API responses do not contain numerical codes by default. The GraphQL spec leaves it up to GraphQL tools to show / not-show GraphQL errors.

This makes it important for people working with GraphQL to understand the errors and how these errors are handled by their GraphQL tool of choice.

In this article, I will cover:

  • A quick introduction to common errors experienced in GraphQL APIs
  • How to handle GraphQL errors while building APIs with Hasura
  • Building custom error pages on a client side React app

Errors: REST Vs GraphQL

REST API’s use various API response codes which are returned with every API  request to tell the users/developers what happened to their request.  This is kind of obvious to someone working with REST, but GraphQL doesn’t work that way.

GraphQL responses do not contain numerical codes by default, and in case of an error, return an errors array with description of what went wrong. See the sample errors array below:

"errors": [{
      "message": "Name for character with ID 1002 could not be fetched.",
      "locations": [ { "line": 6, "column": 7 } ],
      "path": [ "hero", "heroFriends", 1, "name" ]
  }]

The GraphQL spec generally discourages adding properties to error objects, but does allow for it by nesting those entries in an extensions object.

GraphQL services may provide an additional entry to errors with key extensions. This entry, if set, must have a map as its value. This entry is  reserved for implementers to add additional information to errors however they see fit, and there are no additional restrictions on its  contents. (docs).

This extensions object is used by GraphQL servers (including Hasura) to add  additional properties to the errors object. For example, sample errors array returned by Hasura looks like this:

“errors”: [{
 “extensions”: {
   “path”: “$.selectionSet.post.selectionSet.name”,
   “code”: “validation-failed”
 },
   “message”: “field \”name\” not found in type: ‘post’”
 }]

Hasura returns a extensions.code  object which can be used to classify errors and show appropriate response on the client side. In this post, I will use this object to show custom error-pages on a React client.

But first let’s learn  about the common GraphQL errors and how they are handled in Hasura.

An introduction to GraphQL Errors

GraphQL Errors fall into the following categories :

  • Server errors: These  include errors like 5xx HTTP codes and 1xxx WebSocket codes. Whenever  server error occurs, server is generally aware that it is on error or is  incapable of performing the request. Server errors may also occur due  to closing of websocket connection between client and server, which may  happen due to various reasons (see CloseEvent for different types of 1xxx error codes). No data is returned in this case as GraphQL endpoint is not reached.
  • Client errors:  These include errors like malformed headers sent by client,  unauthorized client, request timeout, rate-limited api, request resource  deleted, etc. All the client errors return 4xx HTTP codes. Same with  server errors, no data is returned.
  • Error in parse/validation phase of query:  These include errors while parsing the GraphQL query. For example, if  client sends malformed GraphQL request, i.e. syntax error. Or if the  query does not pass GraphQL internal validation, i.e. client sent inputs  that failed GraphQL type checking. In both these cases, no partial data  can be returned. In case of validation error, errors  array is returned showing what went wrong, while queries with syntax  errors are usually not sent to GraphQL endpoint and are caught at the  client side.
  • Errors thrown within the resolvers:  Resolver errors may occur due to lots of reasons, depending on the  implementation of resolver functions. For example, errors like poorly  written database queries, or errors thrown on purpose like restricting  users from particular countries to access some data. Most importantly,  these kind of errors can return partial data/fields that are resolved  successfully alongside an error message.

Some of these errors do not apply for Hasura GraphQL Engine.  For example, resolver errors (unless you are writing custom resolvers,  in which case you have to take care of the code being error-free).

Special case GraphQL errors with Hasura

There are 2 cases in which Hasura itself will throw errors:

Directly altering tables / views: If the tables/views tracked by the GraphQL engine are directly altered using psql or any other PostgreSQL client, Hasura will throw errors. To troubleshooting those errors, see hasura docs.

Partial Data: Hasura enforces query completeness - a query that returns incomplete data will fail. Partial data is returned only if the query/mutation deals with remote schema, depending on the resolvers written by developers.

Now let’s jump into implementation of error pages.

Custom error handling with React

For implementing error pages, I will use the code from a hackernews-clone app I made as boilerplate. You can easily follow along and add error pages in your app  accordingly. The final code is hosted here.

404 resource not found error

Let’s first start by simply adding a 404 resource not found error page, which is shown when user goes to any unspecified route. This can be simply achieved using routing alone. In App.js, we have to make the following changes:

Notice  that you just have to add a wild card Route with and asterisk(‘*’) in  the end, which matches if any other routes does not match.

Now we can create the NotFound component as :

We get a 404 error whenever user enters an unspecified route/url:

Any unspecified route is matched with asterisk(*) and 404 page is shown

Network Errors / Server Errors

Network Errors are errors that are thrown outside of your resolvers. If networkError is present in your response, it means your entire query was rejected,  and therefore no data was returned. Any error during the link execution  or server response is network error.

For example, the client failed to connect to your GraphQL endpoint, or some error occurred within your request middleware.

The best way to catch network errors is to do it on top-level using  the apollo-link-error library. apollo-link-error can be used to catch and handle server errors, network errors, and GraphQL errors. apollo-link-error can also be used to do some custom logic when a GraphQL or Network error occurs.

Now let’s implement network-error page using apollo-link-error. In App.js, we have to make the following changes:

Note that in line 8, we have intentionally changed the GraphQL endpoint uri to replicate a network error. We defined onError which catches both graphQLErrors and networkErrors and allows us to do custom logic when error occurs. Every time networkError occurs, the if statement in line 18 is executed, and we redirect the user to a network-error page using react-router history prop (see line 20). In most simple terms, history object stores session history, which is used by react-router to navigate to different paths.

We push the path network-error on history object, and we have defined the path in routes (line 32). Thus, when the if statement executes, the user is automatically redirected to /network-error url.

See this thread to better understand react-router and history object. Note that we are using withRouter in App.js to get access to the history object through props.

We will now create NetworkError component as:

We get a network error, whenever the client cannot connect to the server:

Whenever a network error occurs, user is redirected to /network-error

GraphQL Errors

Hasura provides various API’s, but our react client will make requests to the GraphQL API.

Hasura GraphQL API

All GraphQL requests for queries, subscriptions and mutations are made to the Hasura GraphQL API. All requests are POST requests to the /v1/graphql endpoint.

The /v1/graphql endpoint returns HTTP 200 status codes for all responses.

Any error that is thrown by the Hasura GraphQL API, will fall under GraphQL Errors. Hasura GraphQL API throws errors, returning an errors array having errors[i].extensions.code field with pre-defined codes. These codes can be used to classify errors and do custom logic accordingly.

Note: Hasura GraphQL API errors-codes are not documented currently, see this open issue for more information.

Apollo-link-error and apollo/react-hooks make GraphQL error handling easy for us. By default, we want our app to  display global error pages (for example, a page with message “oops  something went wrong”) whenever we encounter some query-validation errors or data-exception errors. But we also want the flexibility to be able to handle an error in a specific component if we wanted to.

For  example, if a user was trying to upvote an already upvoted post, we  want to display an error message in-context with some notification bar,  rather than flash over to an error page.

Handling errors at top level

Top level errors can be handled using apollo-link-error library. For example, if we are trying to query a field which is not present, a validation-failed error would be returned by Hasura GraphQL API. Or trying to mutate a field with string value but the field accepts an integer, data-exception error will be thrown.

Example error responses returned by Hasura GraphQL API:

{
 “errors”: [{
    “extensions”: {
    “path”: “$.selectionSet.dogs.selectionSet.name”,
    “code”: “validation-failed”
   },
   “message”: “field \”name\” not found in type: ‘dogs’”
 }]
}{
  "errors": [{
      "extensions": {
        "path": "$.selectionSet.insert_dogs.args.objects",
        "code": "data-exception"
      },
      "message": "invalid input syntax for integer: \"a\""
    }]
}

These  are errors for which developer is at fault, and the end-users will  probably not understand what went wrong, if shown the above error  messages. In other words, these error messages are meant to help  developers. In such cases, it’s a good idea to use top-level error pages  which shows a “something went wrong” message. We will implement the  same using apollo-link-error.

In App.js, we have to make the following changes:

Every time a graphQLError occurs, if block in line 7 gets executed, which triggers the switch case with extensions.code as the switch expression, thus we can map error-codes to logic we want to perform. Note that I haven’t put a break statement after data-exception (line 10) as I want to show same error-page on both data-exception and validation-failed errors. We redirect end-user to /something-went-wrong route in case of these errors.

We will now create SomethingWentWrong component as:

On validation-failed error, we get a “something went wrong” page:

Custom logic on certain errors

We can also do some custom logic in case of certain error rather than redirecting to error-pages.

For example, if the error occurs while validating JWT (jwt’s are used for authentication), or if the JWT has expired, we can write custom logic to refetch the JWT, and send back the api request. Errors array:

{
  "errors": [{
        "extensions": {
        "path": "$",
        "code": "invalid-jwt"
      },
      "message": "Could not verify JWT: JWSError (JSONDecodeError  \"protected header contains invalid JSON\")"
  }]
}{
  "errors": [{
        "extensions": {
        "path": "$",
        "code": "invalid-jwt"
      },
      "message": "Could not verify JWT: JWTExpired"
  }]
}

We will now write custom logic to handle these errors. In App.js, we will make the following changes:

If the error-code is invalid-jwt, we refetch the JWT and try the API request again with new authorization header.

Here is a diagram of how the request flow looks like now:

Handling Errors at component level

Errors can also be handled at component level, using functions provided by apollo-react-hooks.  There may be many reasons why we would want to handle errors at  component level, for example, you may want to do some component-level  logic, or display notifications if some particular error happens.

Here,  we will be handling unique key constraint violation error, which is  preventing a user to upvote an already upvoted post. Error array  returned by Hasura GraphQL API:

{
  “errors”:[{
      “extensions”: {
      “path”:”$.selectionSet.insert_point.args.objects”,
      ”code”:”constraint-violation”
    },
    ”message”:”Uniqueness violation. duplicate key value violates unique constraint \”point_user_id_post_id_key\””
  }]
}

We have a post component which is using apollo/react-hooks function useMutation to mutate data on the server. When the above error is thrown, we catch the error and check for error-code.

We can access errors array returned by Hasura using error.graphQLErrors. Note that the errors array may contain more than one error, so we are iterating over the array to check if the error code constraint-violation is present. If a match is found, we show a toast notification with error message.

I am using react-toastify to show error notifications. Now, whenever user tries to upvote a post already upvoted by him/her, error notification pops up:

Whenever user tries to upvote already upvoted post, error notification is shown

Error policies (apollo client)

At  last, if you are writing custom resolvers and using remote schemas with  Hasura, your queries/mutation may return partial data with errors  depending on implementation of the resolvers. In that case, apollo errorPolicy may come handy.

You can simply set errorPolicy on each request like so:

const { loading, error, data } = useQuery(MY_QUERY, { errorPolicy: 'all' });

Now, if server returns partial data and error, both data and error can be recorded and shown to user. Check out this link to know more about errorPolicy.

Summary

You now know how to handle errors while building GraphQL APIs using the Hasura GraphQL Engine. If you have any comments, suggestions or questions - feel free to let me know below.

References:

About the author

Abhijeet Singh is a developer who works across a range of topics including fullstack Development, Android, Deep  Learning, Machine Learning and NLP. He actively takes part in  competitive programming contests and has interest  in solving  algorithmic problems. He is a startup enthusiast and plays  table tennis  and guitar in spare time.


Hasura is an open-source engine that gives you realtime GraphQL APIs on new or existing Postgres databases, with built-in support for stitching custom GraphQL APIs and triggering webhooks on database changes.


PS: We’re hiring!