Handling and documenting GraphQL errors using Apollo-Prophecy + Apollo

July 16, 2018 0 Comments

Handling and documenting GraphQL errors using Apollo-Prophecy + Apollo

 

 

You shall fail… successfully

Errors are common to all computer programs; they might be hard to maintain, but properly dealing with them is without any doubt the most critical part of building applications.

In the context of a Client/Server architecture we need the Server to output well-formatted and easily identifiable errors that the Client can seamlessly read, process and handle in order to fail successfully.

E.g. a successful failure

GraphQL powered APIs are no Exceptions (pun intentional 😏) to this rule. Here is what the latest draft (Sun, Jun 10, 2018) of the GraphQL specification says about how error outputs should be formatted.

If the operation encountered any errors, the response map must contain an entry with key errors.
Every error must contain an entry with the key message with a string description of the error intended for the developer as a guide to understand and correct the error.
GraphQL services may provide an additional entry to errors with key extensions. This entry, if set, must have a map as its value. This entry is reserved for implementors to add additional information to errors however they see fit, and there are no additional restrictions on its contents.

With this in mind, a typical error object should look something like this:

...
"errors": [
{
"message": "Only Prophets can do this",
"locations": [ ... ],
"path": [ ... ],
"extensions": {
"code": "NOTAPROPHET",
"timestamp": "Thu Jun 21 17:03:00 UTC 2018"
}
}
]
...

Remember that we want the error output to be “well-formatted and easily identifiable” meaning it should contain at least one field than can be seamlessly processed by a computer.

First candidate to consider is message, a “string description of the error intended for the developer[…]”. Since it is formatted to be read by a human, it could potentially be an expressive long string containing unwanted characters (%, ç, à, $, €, @, white spaces, etc…) thus not ideal.

According to the specification, extensions should be the dedicated space for any additional entry to errors. Here, it gives us the ability to attach a code key, providing a machine-readable datum that can be seamlessly read, processed and handled”.

if (error.extensions.code === "NOTAPROPHET") {
// Do Something
}

We just saw that there are guidelines on how to output errors in the context of a GraphQL API. With that we should be able to:

  • Throw and output spec-compliant and identifiable errors — thanks to extensions — within our resolvers.
  • Identify and handle errors client-side to fail successfully.

However, the specification doesn’t specify guidelines for issues like APIs errors documentation, retry or failure handling, meaning that there are countless ways to properly arrange our code base for that purpose.

The absence of explicit convention led me to build Apollo-Prophecy.

First, let’s illustrate what maintaining errors can be like without Apollo-Prophecy. To that end we’ll be using Apollo Server, a prominent, spec-compliant, fully-featured and well-maintained GraphQL server implementation for nodeJS.

Because we’re using Apollo Server, we can use the constructor ApolloError(message, code): errors thrown using this constructor produce a spec-compliant JSON output like the one above.

throw new ApolloError("Only Prophets can do this", "NOTAPROPHET");

In order to make it easier for us to store errors we could organize our server-side code the following way:

server

And properly handle errors like this:

client

Done, right?

No, we can do better. With this configuration, we end up doing the same work twice: since for every existing error entry on the server we would have to write a corresponding key client side.

I don’t know about you but I prefer to say DRY.

One of the most interesting propositions of GraphQL is that API should be self-documenting. While this is usually done through a mechanism named “introspection queries” — giving us detailed information about the fields and types in our schema — this doesn’t mean that we cannot add documentation material to the schema itself:

What if errors were part of the schema?

Here is how we could exploit this:

1. We include errors in the schema:

type ErrorExtensions {
code: String!
}
type Error {
name: String!
message: String
extensions: ErrorExtensions
}
type Query {
...
errors: [Error!]!
...
}

2. We create the corresponding resolver on the Query field:

...
const resolvers = {
Query: {
...
errors: { ... }
}
}
...

Assuming that information about errors are accessible through our APIs, we need to find a way to access them from the client, keeping in mind that we want to avoid doing the same work twice.

From here we can discuss two different implementations:

  1. Every time our app launches, the client could perform a query to fetch all errors codes and store them locally. 😒 Meh…
  2. Handle it on the dev-side by fetching and storing errors statically in the codebase as part of the build process. 💁 Why not?

Since correct error-handling is critical to the good functioning of your application, going with option 1 would make fetching all errors’ definitions a mandatory step of the app launch process — therefore increasing loading duration.

That’s why for cleanliness and overall performance, I like the second option better.

I’ve started working on Apollo Prophecy: a code generation Command Line interface that does what we need (and a tiny bit more!). It will:

  • Generate errors that we can throw in our resolvers and expose through the schema as documentation — apollo-prophecy generate
  • Query the server schema and generate file with methods and helpers to gracefully consume errors — apollo-prophecy ask

The goal is to always keep you server and client errors repository in sync.

First, install it through your favorite package manager.

[npm | yarn] install -g apollo-prophecy

The generate command will create a file containing throwable error classes. It takes as input a JSON file formatted like this:

Map of objects whose keys are error names, each of those object has a key “message" (optional) and "code"

It can be run like below (if nothing is specified it will look for an errors.json file inside the running folder):

apollo-prophecy generate errorsDef.json

Using the above errosDef.json the CLI will generate the following file.

Here are the generated file key components:

  • errorsList — plain JSON array meant to be used as documentation output. It contains all error representations with their static data: name, message, extensions -> code. Always generated but empty if there is no error to generate.
  • errorType — GraphQL object type that we can include in our schema definition. It should be used alongside errorsList for documentation. Always generated as is.
  • PropheticError — class extending ApolloError meant to be inherited by other errors in this file. Always generated as is.
  • NotAProphetError ProphetNotFoundWithId — those are the two custom error classes generated with the information of the JSON file input.

We can use all these elements in our server. Given that we need errors to be part of our schema, we can do the following:

import { errorsList, NotAProphetError } from './gen/GeneratedErrors'
Query: {
errors: () => errorsList
getAllUsers: () => {...throw new NotAProphetError()},
}

Not yet; prophets need to communicate with gods in order to anticipate the future, don’t they? Using Apollo-Prophecy, we can do something similar with the command ask:

apollo-prophecy ask http://localhost:3000/graphql [--field]

This will send a request to the specified endpoint and try to perform a GraphQL query on the --field option to try and fetch errors’ information (if nothing is specified, an “errors” field will be queried by default).

Below is an extremely simplified version of the generated file. If you want to have an idea of what it really looks like go and try it yourself!

  • PropheticErrorCode —an enum with the codes of all errors exposed in the schema.
  • errorHere and isThis are the real two helper methods that enable us to handle client-side errors in a clean and reusable way.

When called, it returns an object that has one property named after each errors found on the server. Depending on the supplied argument, the called property returns either true or false:

import { errorHere } from ./_generated/Errors.ts;
...(error) => {
if(errorHere(error).isNotAProphetError){
// Do something
} else if(errorHere(error).isProphetNotFoundWithId){
// Do something else
}
}

When called, it returns an object that has one handler function named after each errors found on the server.

import { isThis } from ./_generated/Errors.ts;
...(error) => {
isThis(error)
.UserNotFoundError(() => ...)
.NotAProphetError(() => ...)
.handle()
}

Handlers return the same instance object than isThis, so that each function call can be chained. Once the handle method is called, it initiates the check and calls the corresponding handler if there is a match.

And… voilà! Thanks to the ask command we can keep our client-side error repository in sync with the API through the schema. By using errorHere and isThis we now have a clean and expressive way of handling errors — and look, the code is pretty too!

Just like any young technology, GraphQL still has gaps to fill. Apollo-Prophecy is built to fill just one of these gaps: how we implement error handling and documentation. But this isn’t the end of the conversation; Apollo-Prophecy is open-source, and I’m sure together we can come up with even better ways to improve it.

Already there is a lot of work and fixes to be done on Apollo-Prophecy; contributions and suggestions are both welcomed and needed. Please visit Github and take look at existing issues or even create new ones.

If you’ve come this far, thank you for reading ❤️ I really hope you enjoyed this post and I’d love to hear your thoughts and feedback 🙂.


Tag cloud