DEV Community

André König
André König

Posted on • Updated on

Handling errors in GraphQL

There has been some discussions recently about how to handle errors in GraphQL resolvers. I mentioned apollo-errors because I had very good experiences with it. In this article, I want to take the chance and describe my approach of handling errors in a GraphQL API.

Anatomy of an error

Before diving deep into how to establish a proper error handling, I would like to differentiate a little bit what kind of errors we are talking about here. Basically, as in all user-facing systems, there are two possible error types:

  • Controlled errors: An exception which indicates that the user did something wrong (e.g. Wrong login credentials, etc.)
  • Uncontrolled errors: The counterpart. An exception that indicates that something really bad happened (e.g. storage system not available, etc.)

The ones we are most interested in are the controlled errors. These are the ones which you as the software engineer define and throw when the particular case has happened. Uncontrolled errors, as the name states, are the ones which can happen all the time. Even if they are not controllable, you will learn how to handle them gracefully as well.

The common approach

When reading about GraphQL, you will often see the following example:

if (!areCredentialsValid) {
    throw new Error("Authentication required");
}

This is a simple approach and might be sufficient in most cases. The downside here is that a respective client has a hard time figuring what kind of an error this actually is. A err.message === "Authentication required" in the client is not cool at all.

It would be great to have a kind of error type, right? This is where apollo-errors comes to play. Let's go!

A more robust approach

Let's consider the following scenario: We have a login mutation and we want to throw an error when the user entered wrong credentials. A possible type could be WrongCredentialsError. The first step to do is creating the actual error type:

// path: resolvers/mutation/login/errors/WrongCredentialsError.ts

import { createError } from "apollo-errors";

const WrongCredentialsError = createError("WrongCredentialsError", {
    message: "The provided credentials are invalid."
});

export { WrongCredentialsError }

Now where we have our error type, we can throw it from our login mutation resolver:

// path: resolvers/mutation/login/index.ts

import { WrongCredentialsError } from "./errors/WrongCredentialsError";

interface LoginInput {
  username: string;
  password: string;
}

const login = await (parent, args: LoginInput, context: Context, info) {
    const user = await context.db.query.user({where: {username: args.username}});

    const areCredentialsValid = checkCredentials(user, args.password);

    if (!areCredentialsValid) {
        throw new WrongCredentialsError();
    }
};

So when this mutation gets executed and the user entered wrong credentials the GraphQL API would respond with:

{
  "data": {},
  "errors": [
    {
      "message":"The provided credentials are invalid.",
      "name":"WrongCredentialsError",
      "time_thrown":"2018-02-14T00:40:50.954Z",
    }
  ]
}

Pretty, isn't it? So, in theory the GraphQL API would respond with this. There is one missing puzzle piece. Due to the different error structure, we have to tell the GraphQL API endpoint that those errors should be formatted differently.

But no worries, installing the formatter is an one-time shot and easily done. The following describes how to hook the formatter up on a graphql-yoga based application.

import { GraphQLServer, Options } from "graphql-yoga";
import { formatError } from "apollo-errors";

const options: Options = {
  formatError
};

const server = new GraphQLServer({ typeDefs, resolvers })
server.start(options, () => console.log('GraphQL API is running on localhost:4000'))

That's it! You're able to throw named controlled errors now.

Handling uncontrolled errors gracefully

As promised above, I mentioned to give you an approach to handle uncontrolled errors gracefully as well. You may have read from the code snippets, that I use Prisma for interacting with my database. Let us assume that I've messed something up (e.g. sent a stuffy query to my database, etc.). In those cases, where something really bad happened, we want to inform the client that a FatalError occurred. How can we achieve this?

One approach would be to put each Prisma interaction in a try / catch and throw the FatalError there. That would work, but is pretty cumbersome because you have to handle that in all your resolvers.

The other approach could be to wrap all our resolvers into a wrapper that executes the actual resolver and checks if that resolver didn't throw an uncontrolled error. Let us call this wrapper helmet. It could look like:

// path: resolvers/helmet.ts

import { FatalError } from "./errors/FatalError";

const helmet = (resolver) => async (...args) => {
  try {
    //
    // Try to execute the actual resolver and return
    // the result immediately.
    //
    return await resolver(...args);
  } catch (err) {
    //
    // Due to the fact that we are using Prisma, we can assume
    // that each error from this layer has a `path` attribute.
    //
    // Note: The `FatalError` has been created before by
    // using `apollo-errors` `createError` function.
    //
    if (err.path) {
      throw new FatalError({ data: { reason: err.message } });
    } else {
      throw err;
    }
  }
};

export { helmet };

In order to actually handle the uncontrolled errors gracefully, you have to wrap your resolvers into that helmet function. Here for example, we use the login mutation described above:

// path: resolvers/mutation/index.ts

import { helmet } from "../helmet";

import { login } from "./login";

const Mutation = {
  login: helmet(login)
  // ...
};

export { Mutation };

Conclusion

Even when just throwing an Error object right away might be sane in some scenarios, I would suggest considering an approach like the one described in this post for larger applications. Remember, one of GraphQL's strengths is its expressiveness. Why shouldn't you treat your errors also like that?

I hope you enjoyed the post and I'm happy to hear your thoughts :)

Top comments (7)

Collapse
 
prestongarno profile image
Preston

Coincidentally I was reading through that exact part of the spec on errors yesterday and found a few neat things:

  • There is only one requirement for the relationship between data and errors return value: if the query is null/empty there must be an error message

  • The spec recommends that implementations provide a char index in the query in the error message where the error occurred. I don't think all servers I've tried follow this, but the major ones do IME

Overall, the GraphQl spec doesn't enforce a whole lot with the response format - it doesn't even need to be JSON!

I do like that the rules needed to have reliable interaction are pretty rigorously defined but it leaves room for extension of the protocol.

Collapse
 
jhalborg profile image
jhalborg

Hey André, thanks for a great post! I'll definitely use some of these pointers in my project.

Do you have any strategy for bulk-adding the handler helmet for all resolvers, without having to wrap all of them explicitly with helmet(resolver)? That'd be useful, at least in the beginning of a project before any needs for custom handling depending on resolver is needed, if ever

Collapse
 
spacek33z profile image
Kees Kluskens

Hi, just made an account on dev.to to say thank you for this article! This was a missing piece in the puzzle that is GraphQL for me :).

Collapse
 
andre profile image
André König

That means a lot, Kees. Glad the article was the last puzzle piece. Awesome 🙂

Collapse
 
goodidea profile image
Joseph Thomas

I was pointed here by a question I posted on Spectrum:
spectrum.chat/?t=6f834d41-f2c2-476...
This is exactly what I needed, thank you! 🙌

Collapse
 
andre profile image
André König • Edited

Awesome, glad that you like the article. You are very welcome 🙂

Collapse
 
awoyotoyin profile image
Awoyo Oluwatoyin Stephen

Now this was exactly what I needed. Thanks for posting