DEV Community

loading...
Cover image for Error Handling in GraphQL

Error Handling in GraphQL

gethackteam profile image Roy Derks Originally published at newline.co ・4 min read

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 operation

  • The errors field, containing all the errors that occurred during the execution of the operation

  • An 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
}
Enter fullscreen mode Exit fullscreen mode

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:

GraphQL Default Error Handling

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');

// ...

}
Enter fullscreen mode Exit fullscreen mode

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.

GraphQL Stacktrace

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 resolver

  • Errors 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
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

Discussion

pic
Editor guide