Laurin Quast Software Engineer

Handling GraphQL errors like a champ with unions and interfaces

8 min read 2392

Graph-ql-nocdn.png

Without a doubt, one of the best features of GraphQL is its awesome type system.

Together with tools like the GraphQL Code Generator and typed Javascript subsets like TypeScript or Flow, you can generate fully typed data fetching code within seconds.

I cannot think back to the time where I had to design and build API’s without the GraphQL ecosystem.

When I started using GraphQL, I had some issues with changing the mindset I’d developed by thinking in REST.

One thing I’ve been particularly displeased about is error handling. In traditional HTTP, you have different Status Codes that represent different types of errors (or successes).

When GraphQL was gaining popularity, I remember a meme made of some terminal that showed an Apollo server logging an error object with status code 200 and the caption ok. I was wondering why GraphQL breaks these widely-used standards.

Today, I know that GraphQL gives us the power to handle errors in a better and more explicit way.

Handling errors in GraphQL

Before we take a look at how I design my APIs today, I wanna showcase the evolution of how I was handling errors until recently.

I’ll use react-apollo and apollo-server code examples throughout this article. However, the concepts should be applicable to any other client and server framework.

Let’s start with a look at the following JSON object:

We made a custom demo for .
No really. Click here to check it out.

{
  "errors": [
    {
      "message": "Name for character with ID 1002 could not be fetched.",
      "locations": [ { "line": 6, "column": 7 } ],
      "path": [ "hero", "heroFriends", 1, "name" ]
    }
  ],
  "data": {
    "hero": {
      "name": "R2-D2",
      "heroFriends": [
        {
          "id": "1000",
          "name": "Luke Skywalker"
        },
        {
          "id": "1002",
          "name": null
        },
        {
          "id": "1003",
          "name": "Leia Organa"
        }
      ]
    }
  }
}

Does this seem familiar?

This exact code is copied from the GraphQL Spec Error Section. If you’ve already integrated a GraphQL API into your application, you may be familiar with this response format.

By design, GraphQL has the capabilities to declare fields nullable. Despite this data being optional, it also allows us to send partial results if a resolver throws an error.

This is one thing that differentiates GraphQL from strict REST.

If a resolver throws an error — in this case, the name resolver for the hero with the id 1002 — a new array with the key errors is appended to the response JSON object.

The array contains an error object with the original message of the error, a path, and a query location.

The code for the resolver would look similar to this:

const resolvers = {
  Hero: {
    name: (parent, args, context) => {
      throw new Error(
        "Name for character with ID 1002 could not be fetched."
      );
    },
  },
};

I once thought this was pretty cool.

Then I realized I needed more detailed info — something like a status code or an error code. How would I distinguish a “user does not exist” error from a “user has blocked you” error?

The community learned and the concept of extensions were added to the GraphQL spec.

Extensions are nothing more than an additional object that can be added to your error object (or response object).

{
  "errors": [
    {
      "message": "Name for character with ID 1002 could not be fetched.",
      "locations": [ { "line": 6, "column": 7 } ],
      "path": [ "hero", "heroFriends", 1, "name" ],
      "extensions": {
        "code": "CAN_NOT_FETCH_BY_ID",
        "timestamp": "Fri Feb 9 14:33:09 UTC 2018"
      }
    }
  ]
}

With extensions ,we can add a code property to our error object, which can then be used by the client (e.g. a switch or if statement).

This is way more convenient than parsing the error message for interpreting the error.

Frameworks like the Apollo Server provide Error classes that can be initialized with an error message and a code:

import {
  ApolloError,
}  from "apollo-server";

const resolvers = {
  Hero: {
    name: (parent, args, context) => {
      throw new ApolloError(
        "Name for character with ID 1002 could not be fetched.",
        "CAN_NOT_FETCH_BY_ID",
      );
    },
  },
};

Of course, I also started quickly adopting this style of error handling, but I soon realized that there are some drawbacks that reduce my productivity:

The errors are not colocated to where they occur

Of course, you have a path array that describes where an error occurs (e.g. [ hero, heroFriends, 1, name ]). You can build some custom function in your client that maps an error to your query path.

I personally believe that every error should be handled in the UI of the application.

Having the error located somewhere different by default doesn’t really encourage developers to gracefully handle errors.

Furthermore, frameworks like relay modern encourage you to only inject fragments into your components.

For proper error handling, you need to apply custom logic for injecting the correct error into the correct component.

Sounds like extra work that I personally would want to avoid.

Using the errors robs us of one of the main benefits of GraphQL: type safety

As mentioned earlier, one of the main benefits of a GraphQL API is type safety.

A schema is by default introspectable and exposes a complete register of all the available types and fields.

Unfortunately, the error codes don’t follow any schema (at least not according to the GraphQL spec).

No type error will be thrown if you mistype the error message or extension code inside your resolvers.

The GraphQL engine does not care about the structure of the message.

Furthermore, the error code is just an optional extension. I am not currently aware of any tool that generates type-safe error codes, nor can you see an overview of all available error codes that a field (or resolver) could throw.

When using the errors array, we’re back in good old type guessing land.

Backend and frontend developers now have one more pain to deal with (one that they actually tried to avoid by switching over to GraphQL in the first place.)

Don’t misunderstand me — even if you have a fully-typed GraphQL API, there should still be some documentation.

The API browser generated by tools like GraphiQL or GraphQL Playground should make it easier to discover and understand what a GraphQL API provides, but it shouldn’t replace a documentation with usage examples.

We can do it better with the existing GraphQL primitives

Recently, there’s has been a lot of buzz around using union types for handling errors. A union type represents a list of objects that a field can return.

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

type UserNotFoundError {
  message: String!
}

union UserResult = User | UserNotFoundError

type Query {
  user(id: ID!): UserResult!
}

In the following schema, the field user can either return a User or UserNotFoundError. Instead of throwing an error inside our resolver, we simply return a different type.

The query that you would send to your server would look like this:

query user($id: ID!) {
  user(id: $id) {
    ... on UserNotFoundError {
      message
    }
    ... on User {
      id
      login
    }
  }
}

Accordingly, the apollo-server resolver could look similar to the following:

const resolvers = {
  Query: {
    user: async (parent, args, context) => {
      const userRecord = await context.db.findUserById(args.id);
      if (userRecord) {
        return {
          __typename: "User",
          ...userRecord,
        };
      }
      return {
        __typename: "UserNotFound",
        message: `The user with the id ${args.id} does not exist.`,
      };
    },
  },
};

When using unions, you will have to return a __typename so apollo-server knows which type the result has and which resolver map must be used for resolving further field values of the resolved type.

This allows us to model errors like normal GraphQL types. This way, we regain the power of type safety: instead of working with a message and an error code, we can have more complex types.

Below is an example of a login mutation that returns the UserRegisterInvalidInputError error type.

Despite having a generic error message, the type also provides fields for the single input fields.

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

type UserRegisterResultSuccess {
  user: User!
}

type UserRegisterInvalidInputError {
  message: String!
  loginErrorMessage: String
  emailErrorMessage: String
  passwordErrorMessage: String
}

input UserRegisterInput {
  login: String!
  email: String!
  password: String!
}

union UserRegisterResult = UserRegisterResultSuccess | UserRegisterInvalidInputError

type Mutation {
  userRegister(input: UserRegisterInput!): UserRegisterResult!
}

You could even go further and add fields that return new, more complex object types.

A client implementation could look like similar to this:

import React, { useState } from "react";
import { useUserRegisterMutation } from "./generated-types"
import idx from "idx";
import { useFormState } from 'react-use-form-state';

const RegistrationForm: React.FC<{}> = () => {
  const [userRegister, { loading, data }] = useUserRegisterMutation();
  const loginState = useFormState("login");
  const emailState = useFormState("email");
  const passwordState = useFormState("password");

  useEffect(() => {
    if (idx(data, d => d.userRegister.__typename) === "UserRegisterResultSuccess") {
      alert("registration success!");
    }
  }, [data]);

  return (
    <form
      onSubmit={(ev) => {
        ev.preventDefault();
        userRegister();
      }}
    >
      <InputField
        {...loginState}
        error={idx(data, d => d.userRegister.loginErrorMessage)}
      />
      <InputField
        {...emailState}
        error={idx(data, d => d.userRegister.emailErrorMessage)}
      />
      <InputField
        {...passwordState}
        error={idx(data, d => d.userRegister.passwordErrorMessage)}
      />
      <SubmitButton />
      {idx(data, d => d.userRegister.message) || null}
      {loading ? <LoadingSpinner /> : null}
    </form>
  )
}

GraphQL gives you the power to shape your data tree according to your UI

That’s why you should also shape your error types according to the UI.

In case you have different types of errors, you can create a type for each of them and add them to your union list:

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

type UserRegisterResultSuccess {
  user: User!
}

type UserRegisterInvalidInputError {
  message: String!
  loginErrorMessage: String
  emailErrorMessage: String
  passwordErrorMessage: String
}

type CountryBlockedError {
  message: String!
}

type UserRegisterInput {
  login: String!
  email: String!
  password: String!
}

union UserRegisterResult =
  UserRegisterResultSuccess
  | UserRegisterInvalidInputError
  | CountryBlockedError

type Mutation {
  userRegister(input: UserRegisterInput!): UserRegisterResult!
}

This allows each error type to have their unique properties.

Let’s jump over the the frontend part of this requirement:

You have a new requirement for your API: people from country X should not be allowed to register anymore, due to some weird sanctions of the country your company operates from.

Seems pretty straightforward, just add some new types on the backend, right?

Unfortunately, no. The frontend developer will now also have to update his query because a new type of error, that is not covered by any selection set is now being returned.

This means that the following query:

mutation userRegister($input: UserRegisterInput!) {
  userRegister(input: $input) {
    __typename
    ... on UserRegisterResultSuccess {
      user {
        id
        login
      }
    }
    ... on UserRegisterInvalidInputError {
      message
      loginErrorMessage
      emailErrorMessage
      passwordErrorMessage
    }
  }
}

Needs to be updated to this:

mutation userRegister($input: UserRegisterInput!) {
  userRegister(input: $input) {
    __typename
    ... on UserRegisterResultSuccess {
      user {
        id
        login
      }
    }
    ... on UserRegisterInvalidInputError {
      message
      loginErrorMessage
      emailErrorMessage
      passwordErrorMessage
    }
    ... on CountryBlockedError {
      message
    }
  }
}

Otherwise, the client will not receive an error message for the CountryBlockedError that can be displayed.

Forcing the developer of the client application to adjust their GraphQL documents every time we add some new error type does not seem like a clever solution.

Let’s take a closer look at our error objects:

type UserRegisterInvalidInputError {
  message: String!
  loginErrorMessage: String
  emailErrorMessage: String
  passwordErrorMessage: String
}

type CountryBlockedError {
  message: String!
}

They both have one common property: message

Furthermore, we might assume that every error that will be potentially added to a union in the future will also have a message property.

Fortunately, GraphQL provides us with interfaces, that allow us to describe such an abstraction.

interface Error {
  message: String!
}

An interface describes fields that can be implemented/shared by different types:

interface Node {
  id: ID!
}

type User implements Node {
  id: ID!
  login: String!
}

type Post implements Node {
  id: ID!
  title: String!
  body: String!
}

type Query {
  entity(id: ID!): Node
}

For queries, the power of interfaces lies in being able to declare a data selection trough an interface instead of a type.

That means our previous schema can be transformed into the following:

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

interface Error {
  message: String!
}

type UserRegisterResultSuccess {
  user: User!
}

type UserRegisterInvalidInputError implements Error {
  message: String!
  loginErrorMessage: String
  emailErrorMessage: String
  passwordErrorMessage: String
}

type CountryBlockedError implements Error {
  message: String!
}

type UserRegisterInput {
  login: String!
  email: String!
  password: String!
}

union UserRegisterResult =
  UserRegisterResultSuccess
  | UserRegisterInvalidInputError
  | CountryBlockedError

type Mutation {
  userRegister(input: UserRegisterInput!): UserRegisterResult!
}

Both error types now implement the Error interface.

We can now adjust our query to the following:

mutation userRegister($input: UserRegisterInput!) {
  userRegister(input: $input) {
    __typename
    ... on UserRegisterResultSuccess {
      user {
        id
        login
      }
    }
    ... on Error {
      message
    }
    ... on UserRegisterInvalidInputError {
      loginErrorMessage
      emailErrorMessage
      passwordErrorMessage
    }
  }
}

No need to even declare the CountryBlockedError selection set anymore. It is automatically covered by the Error selection set.

Furthermore, if any new type that implements the Error interface is added to the UserRegisterResult union, the error message will be automatically included in the result.

Of course, you will still have to add some logic on the client for handling your error state, but instead of explicitly handling every single error you can switch between the ones that need some more work, like UserRegisterInvalidInputError, and all these other errors that only show some sort of dialog, like CountryBlockedError.

E.g. if you follow the convention of ending all your error type with the word Error , you can build an abstraction that will handle multiple error types.

import React, { useState } from "react";
import { useUserRegisterMutation } from "./generated-types"
import idx from "idx";
import { useAlert } from "./alert";

const RegistrationForm: React.FC<{}> = () => {
  const [userRegister, { loading, data }] = useUserRegisterMutation();
  const loginState = useFormState("login");
  const emailState = useFormState("email");
  const passwordState = useFormState("password");
  const showAlert = useAlert();

  useEffect(() => {
    const typename = idx(data, d => d.userRegister.__typename)
    if (typename === "UserRegisterResultSuccess") {
      alert("registration success!");
    } else if (typename.endsWith("Error")) {
      showAlert(data.userRegister.message);
    }
  }, [data]);

  return (
    <form
      onSubmit={(ev) => {
        ev.preventDefault();
        userRegister();
      }}
    >
      <InputField
        {...loginState}
        error={idx(data, d => d.userRegister.loginErrorMessage)}
      />
      <InputField
        {...emailState}
        error={idx(data, d => d.userRegister.emailErrorMessage)}
      />
      <InputField
        {...passwordState}
        error={idx(data, d => d.userRegister.passwordErrorMessage)}
      />
      <SubmitButton />
      {loading ? <LoadingSpinner /> : null}
    </form>
  )
}

At a later point in time when your team decides that a new error should be handled different from the others, you can adjust the code by adding a new else/if statement in useEffect.

Conclusion

Code generation tools like the GraphQL Code Generator (or the apollo-cli) can parse your GraphQL schema and will generate TypeScript Definition files for your GraphQL Server.

@dotansimha and me put quite a bit of work into generating correct types for unions and interfaces with GraphQL Code Generator over the last two months. Using such a tool can improve your workflow significantly.

The best thing about GraphQL Codegen is that it can be used with almost every GraphQL client you are working with, whether it is react-apollo, urql or even Java.

I strongly encourage you to check out the GraphQL Codegen as it can further improve your workflow.

Have you already adopted GraphQL? Are you already using unions and interfaces for abstractions? Do you leverage type generation?

Let’s discuss in the comments.

Also, feel free to follow me on these platforms. I write about JavaScript, Node, React, GraphQL and DevOPs.

 

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Laurin Quast Software Engineer

7 Replies to “Handling GraphQL errors like a champ with unions and…”

  1. great article! Question: If i’m returning a custom error from my graphql server do you know how can i return a `locations` array? does graphql ships with some helpers to do it? thanks

  2. I am a bit late to the party, but I have thought hard about your approach. For mutations, I think it is a nobrainer, much cleaner code and we need to define result objects for mutations anyway, so why not use a union of the “normal” return type plus all possible error states? I am not sold completetely on using it for regular queries, for the following reasons:

    – Things get more verbose, you have to add more code to queries. Fragments help here, of course.
    – Say you have a UserResult union that includes a normal User type plus various error types. Do you use that everywhere where a User type might show up? Do you use UserResult only for queries that might return errors and User for queries that do not? The first approach leads to UserResult pervading all of your code but it is consistent, even if you never expect an error. The second avoids that, but then an application performing a query has to be permanently aware wether to expect a User or UserResult object.
    – (this was the deal breaker for me) How do you treat errors when resolving scalar values? Unions cannot include scalar values, so the obvious approach of a union of String and a bunch of error objects is out (and it would make queries *really* verbose). A scalar is normally part of an object, so we could resolve the scalar when resolving the parent object and return a suitable error object if the scalar resolution fails. There are two drawbacks to this a) if resolution of the scalar fails, we error out on the whole object, we cannot return (partial) results from other fields. b) the field might not be part of the query anyway, in which case we resolve it needlessly. That leaves only one more approach, to define a container object for all scalars with a single field for the scalar value and combine it with a union of error objects. This again brings up the problems of verbosity (now *really, really* verbose) and consistency (do we use this *everywhere* for consistency? No “normal” scalars any more?, Or do we mix the two?). So, realistically we are back to null for error values and the standard error array, we have to look for errors in two places and we have to deal with “bubbling up” of nulls when a non null scalar errors out on resolution.

    I think this goes into the right direction but we really need a bit of support from the language here, something like support for a dedicated error object maybe?

    Any thoughts?

  3. @JL Thanks for your comment 🙂

    Regarding “Things get more verbose”:

    One could argue what is more verbose matching the error form the GraphQL error to the component where it occurs or switching over a union type.

    Every solution has its benefits and trade-offs. I usually use those errors for top level fields that resolve a single resource. E.g. for lists or trees where you know that it must exists you can use always use User instead of UserResult.

    Regarding: “How do you treat errors when resolving scalar values”

    This is also dependent on the use-case, why should a single scaler value fail in the first place? It is IMHO dependent on your business logic. Do you need to show an error for a scaler value that fails being displayed? Or could you get away with just returning null.

    I had a situation where I wanted to represent a state-machine. Depending on the state-machine state different properties should be accessible on the type.

    I ended up using a union type with multiple ObjectTypes that have the properties on them as they are available during a certain state. It also seemed a bit strange as during some stages there are no properties to query at all (despite __typename).

    If your business logic relies on a scaler value being unavailable, why not wrap it in a union type, it sounds a bit verbose but it does the job.

    In the end it I think it is a trade-off whether you want “type-safe” errors with a bit of a more verbose schema or error-guessing on the client.

    Regarding “I think this goes into the right direction but we really need a bit of support from the language here, something like support for a dedicated error object maybe?”

    Do you have a solution in mind? Write about it, prototype something and share it!

  4. Hi Laurin,

    “One could argue what is more verbose matching the error form the GraphQL error to the component where it occurs or switching over a union type”

    True, and I think the tradeoff in favor of unions is acceptable here.

    “I usually use those errors for top level fields that resolve a single resource. e. .g. for lists or trees where you know that it must exists you can use always use User instead of UserResult.”

    That is something I would avoid, one object, one result type so that you don’t have to think about which one to use.

    Re Scalar values, business logic and my thoughts in general: I was trying to think about an approach that works regardless of business logic and case by case requirements, i.e. a drop in replacement for the way errors are handled in GraphQL. So I thought about the (admittedly rare) case of resolving a scalar as well.

    “Do you have a solution in mind? Write about it, prototype something and share it!”

    I have been thinking about a special “error” object that is a subtype of all possible types, so that it can appear wherever we expect a scalar or object. It would be what “null” is now for error handling, but with more information about the error attached in place. That means we can have “null” back as a “normal” value and we do not have to deal with non-nullable fields and bubbling up of nulls. One way to go about this would be to use a (previously unused) reserved field name in the error object, like “__isError”. Need to think about this some more … 🙂

Leave a Reply