GraphQL servers are able to handle errors by default, both for syntax and validations errors. You've probably already seen this when using GraphiQL or any other playground to explore GraphQL APIs.
More detailed information on error handling is also available in my new book Fullstack GraphQL.
But often the default way is not sufficient for more complex situations or to sophistically handle the errors from a frontend application. So let's dive into how to improve this.
GraphQL Error Object
Error handling is described in the GraphQL specification and is part of the default structure of any GraphQL response. This response consists of 3 fields:
The
data
field, containing the result of the operationThe
errors
field, containing all the errors that occurred during the execution of the operationAn optional
extensions
field that contains meta data about the operation
The simplest type of error that you can get is when you in example try to use an operation that's not present in the schema of your GraphQL API. Suppose we have an API that serves as the backend for a music application. Using queries and mutations, you will be able to view tracks, add playlists, and save tracks to playlists that you've created.
One of the operations for that API is a mutation to add a track to a playlists, which is called saveTrackToPlaylist
.
saveTrackToPlaylist(input: { playlistId: 1, trackId: 1 }) {
id
title
}
But suppose we make a mistake when spelling the mutation in the GraphQL playground. What would happen? The server will throw a default GraphQL error, as the misspelled mutation is not present in the schema:
The response object has the previously mentioned error
field and a message stating the error: Cannot query field \"saveTrrackToPlaylist\" on type \"Mutation\". Did you mean \"saveTrackToPlaylist\"?
. As you can see GraphQL graciously prevents you in sending non-existing operations to the server. Similar errors are thrown when your operation variables or requested fields are incorrect.
When you have an error that's not related to the GraphQL schema, you would have to throw an error from the resolvers of your GraphQL server. This can simpy be done by throwing an error from the resolver. This would looks something like this for the music application server:
async function saveTrackToPlaylist(
_: any,
{ input }: any,
{ knex }: Context,
): Promise<any> {
if (!knex) throw new Error('Not connected to the database');
const { playlistId, trackId } = input;
const playlist = await knex('playlists').where('id', playlistId).select();
const track = await knex('tracks').where('id', trackId).select();
if (!playlist.length) throw new Error('Playlist not found');
if (!track.length) throw new Error('Track not found');
// ...
}
In the resolver an error is thrown when the playlist or track that you define for the saveTrackToPlaylist
mutation are not present in the database. The error message will again be added to the errors
field of the GraphQL response object.
The snippet above has been truncated but the actual response in the GraphQL playground is a huge chunk of JSON, which makes it hard to get the important information from that response.
Fortunately, the GraphQL specification allows you to add a field called extensions
to the error
object. To extend the error
object, you can either throw a custom Error
object or use predefined error methods available in Apollo Server. Apollo Server provides several predefined errors, such as AuthenticationError
, ForbiddenError
, and UserInputError
, as well as the general ApolloError
. These errors make it easier for you to debug and read errors from the GraphQL server.
Data as error
Handling errors this way works especially well when you're using Apollo to create your GraphQL server, but there are more declarative ways of returning errors to the client. One way of doing so is by adding errors as data. This approach has several advantages:
You no longer have to return
null
for the payload of the operations in the GraphQL resolverErrors become available in the GraphQL schema, and you can include them in the response.
To add errors to your data, you need to use the Union
type (a.k.a. Result
) in your GraphQL schema. Also, the logic in the resolver is rewritten so that next to the result or error for the operation you also need to return a type for it.
This allows you to send the previous mutation in the following way, which has different payload types based on the return of the resolver. The mutation saveTrackToPlaylist
now has three different payloads depending on the result of the resolver.
saveTrackToPlaylist(input: { playlistId: 1, trackId: 1 }) {
... on SaveTrackSuccess {
playlistId
playlistTitle
trackId
trackTitle
}
... on SaveTrackPlaylistError {
playlistId
message
}
... on SaveTrackError {
trackId
message
}
}
The payload SaveTrackSuccess
returns the playlist and tracks details when the operation is successful, while the SaveTrackPlaylistError
and SaveTrackError
are retuned when an error occurs.
More implementations for error handling in GraphQL and the full source code are available it the book Fullstack GraphQL.
Top comments (0)