This tutorial was written and published as part of the Hasura Technical Writer Program
Table of Contents
- Introduction
- Errors: REST Vs GraphQL
- An introduction to GraphQL Errors
- Custom error handling with React
- GraphQL Errors
- Error policies (apollo client)
- Summary
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:
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
andhistory
object. Note that we are usingwithRouter
inApp.js
to get access to thehistory
object throughprops
.
We will now create NetworkError
component as:
We get a network error, whenever the client cannot connect to the server:
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:
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:
- The Definitive Guide to Handling GraphQL Errors
- Full Stack Error Handling with GraphQL and Apollo
- Error handling
- Error images: https://dribbble.com/mayankdhawan/projects/567197-UrbanClap-Empty-States
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.
Top comments (0)