GraphQL errors: the Good, the Bad and the Ugly
Tags: graphql, devjournal, javascript, tutorial
Published: 2023-01-05 15:18:39

We, at Escape, have been using GraphQL for our apps for a long time, before many quality tutorials were available. Because we lacked experience, we made design mistakes on many aspects of our GraphQL API. **This article reviews the evolution of how we return errors from our API,** for consumption by the frontend and other internal services, **emphasizing what could be improved on each step.**

To illustrate this article, we will take the example of an account creation mutation:

```graphql
type Mutation {
  register(email: String!, password: String!): User!
}

type User {
  id: ID!
  email: String!
}
```

> The `register` mutation takes an email and a password and returns the newly created user.

Because the user creation might fail, we need to tell our API consumer (for instance the frontend) that something went wrong and what exactly went wrong. There are many ways to do it, and some work better than others.

# The Ugly: the `errors` field

A GraphQL response is a JSON object with up to two keys: [`data`](https://spec.graphql.org/October2021/#sec-Data) and [`errors`](https://spec.graphql.org/October2021/#sec-Errors). We implemented error responses leveraging the latter. For instance, considering our `register` mutation from before, several errors could be raised:

- The email already exists
- The email uses a banned email provider
- The password is too short
- Unexpected runtime errors (*e.g.* a database connection failed)

The specification indicates that `errors` is an array of objects with a `message` field but allows additional details in an `extensions` field. We used `extensions.code` to convey a machine-interpretable response:

```js
{
  "errors": [
    {
      // User-friendly error message
      "message": "Please provide a professional email address.",
      "extensions": {
        // Machine-friendly error code
      	"code": "PROFESSIONAL_EMAIL_REQUIRED"
      }
    }
  ]
}
```

Several problems emerge from this approach. The most annoying one is that `errors` is an array: the consumer has to iterate over it to collect the errors. What to do if the array contains several errors but none that can be handled by our frontend? This led to **hard to maintain** `switch` inside `for` loops, with fallback cases afterwards.

On the plus side, all of our errors are returned using the same mechanism, leading to a simpler implementation on the backend, but we have a lot of room for improvement.

# The Bad: an `Error` type

GraphQL has a neat type system with a feature named [Union types](https://graphql.org/learn/schema/#union-types). It allows returning several object types from the same resolver. Let's refactor our resolver a bit to include an error type:

```graphql
type Mutation {
  register(email: String!, password: String!): RegisterResult!
}

union RegisterResult = User | Error

type User {
  id: ID!
  email: String!
}

type Error {
  message: String!
}
```

> `register` may now return a user or an error.

Our type definition is getting significantly more complicated, but that's for good! The registration query would now look like this:

```graphql
mutation {
  register(email: "gautier@example.com", password: "p4ssw0rd") {
    __typename
    # Query different fields depending on the response type
    ...on User {
      id
    }
    ...on Error {
      message
    }
  }
}
```

In case the user provides a password that is too short, the response payload would look like this:

```js
{
  "data": {
    "register": {
      // This allows the consumer to know which fields are available
      "__typename": "Error",
      "message": "Password too short."
    }
  }
}
```

**This approach requires us to classify errors in two categories**: the ones we want in the response `errors` field, and the ones we return as an `Error` type. There are two concepts to account for when making this distinction:

1. Some errors can be returned for both queries and mutations, others are exclusive to this specific mutation.
2. Some errors are [actionable](https://developers.google.com/chat/design/errors), some are not.

We consider errors that can be returned for queries too because we want to keep queries short:

```graphql
 This is short and neat
query {
  # User "1" might not exist but let's ignore it for now
  user(id: 1) {
    posts {
      title # ✨
    }
  }
}

# This is bloated and cumbersome
query {
  user(id: 1) {
    __typename
    ...on User {
      posts {
        # Consider possible errors on all fields, even the nested ones
        __typename
        ...on Post {
          title # 😫
        }
        ...on Error {
          message
        }
      }
    }
    ...on Error {
      message
    }
  }
}
```

Therefore, errors such as _User not found_, _Unauthorized_ or runtime errors should still be returned in the `errors` response field, to have query and mutation responses handled the same way.

Furthermore, actionable errors are part of the normal execution flow. It makes sense for a registration to fail and having an error response type associated to it.

Let's take our list of errors from above and categorize them:

- The email already exists: specific to this resolver and actionable → `Error` type
- The email uses a banned email provider: same → `Error` type
- The password is too short: same → `Error` type
- Unexpected runtime errors: can happen for both queries and - mutations and hardly actionable for frontend users → `errors` field

These categories are quite simple to use in the frontend: if `__typename` is `Error`, display the error above the form, otherwise, show a generic “Something went wrong, please try again” error.

![An email/password form with a "Password too short" error above the form](https://escape.tech/blog/content/images/size/w1600/2022/12/register-1.png)

> This error should be next to the password field, but the frontend has no way to know where to place it.

This solution is however less flexible than the previous one: we can only send one actionable error at a time, even when multiple errors could be returned at the same time. We might also want to be able to place the error message right next to its related form input.

# The Good: structured errors

The API might return different kinds of errors, with specific data attached. Let's create **structured errors** for our actionable errors from before. The goal is to replace the generic `Error` type with more specific _domain_ errors.

```graphql
type Mutation {
  register(email: String!, password: String!): RegisterResult!
}

# Use different types for all possible actionable errors
union RegisterResult = User | ValidationError | ProfessionalEmailRequired

type User {
  id: ID!
  email: String!
}

# Malformed inputs
type ValidationError {
  # Allow several errors at the same time!
  fieldErrors: [FieldError!]!
}

type FieldError {
  path: String!
  message: String!
}

# Business specific errors (e.g. banned email providers)
type ProfessionalEmailRequired {
  provider: String!
}
```

When creating our error types, we took into consideration that some errors might be _multiple_: for instance, the validation step might throw several errors at the same time (email already used, password too short _etc._). That is why the error contains an array of `fieldErrors`.

By requesting all the possible errors, it is now possible for the frontend to display contextualized errors (_i.e._ errors next to their input):

```graphql
mutation {
  register(email: "gautier@example.com", password: "p4ssw0rd") {
    __typename
    # Query different fields depending on the response type
    ...on User { id }
    ...on ValidationError {
      fieldErrors { path message }
    }
    ...on ProfessionalEmailRequired { provider }
  }
}
```

```js
{
  "data": {
   	"register": {
      "__typename": "ValidationError",
      // Error messages specific to each input:
      "fieldErrors": [
        {"path": "email", "message": "This account already exists."},
        {"path": "password", "message": "Password too short."},
      ]
    }
  }
}
```

![The same email/password form as before, but both fields have an error: "this account already exists" and "password too short"](https://escape.tech/blog/content/images/size/w1600/2022/12/register-2-2.png)

> The frontend is now able to show several _contextualized_ errors at the same time.

This architecture will also make some future considerations **easier to implement,** especially **internationalization (i18n).**

We are currently refactoring our API to use structured errors. It represents a substantial amount of work, but we are now able to display more precise error messages, especially for complex inputs and flows. **Structured errors help us improve the user experience of our products.**

# HTTP errors

We spent a while talking about error types, but let's get back to the `errors` field to conclude. **Sending errors in here allows keeping queries short and clear,** and we still use it for _Not found_ and _Unauthorized_ errors. With a twist!

The [GraphQL over HTTP specification](https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md) states the following:

> The server SHOULD use the `200` status code, independent of any GraphQL request error or GraphQL field error raised.

We decided to ignore this recommendation and attach semantic HTTP error codes to queries with errors. [Yoga's error masking](https://the-guild.dev/graphql/yoga-server/docs/features/error-masking#modifying-http-status-codes-and-headers) makes it really simple to transform JS error objects into GraphQL errors with the right HTTP code attached:

```js
const yoga = createYoga({
  schema,
  maskedErrors: {
    maskError(error, message) {
      const cause = (error as GraphQLError).originalError;

      // Transform JS error objects into GraphQL errors
      if (cause instanceof UnauthorizedError)
        return new GraphQLError(cause.message, { extensions: { http: { status: 401 } } });

      if (cause instanceof NotFoundError)
        return new GraphQLError(cause.message, { extensions: { http: { status: 404 } } });

      // Default to 500 with a generic message
      return new GraphQLError(message, { extensions: { http: { status: 500 } } });
    },
  },
});
```

This enables the frontend to show the correct HTTP error page in case of a GraphQL error without even parsing the response.

# Read more

We are not the first ones to write about returning GraphQL errors, you might be interested in these articles/documentations too:

- [200 OK! Error Handling in GraphQL | by Sasha Solomon](https://sachee.medium.com/200-ok-error-handling-in-graphql-7ec869aec9bc)
- [GraphQL error handling to the max with Typescript, codegen and fp-ts – The Guild](https://the-guild.dev/blog/graphql-error-handling-with-fp)
- [Errors plugin for Pothos GraphQL](https://pothos-graphql.dev/docs/plugins/errors)

---

## Closing words

This is the end of this _we-do-it-that-way_ article, we hope you enjoyed this format that allows to peep inside our development practices at Escape. We are continuously discovering new ways to design GraphQL APIs and we will keep writing about the different steps we took until reaching the state of the art. Please share your thoughts where you found this article, we have a lot to learn from your experiences!