Errors are results
Tags: graphql, typescript, node, nexus
Published: 2021-01-15 11:01:57

# Errors, Faults & Failures
When writing a server, errors are inevitable, but a distinction should be made between errors caused by things outside the client's control, such as your database crashing - I'll refer to these as Faults. Errors caused by things that are within the client's control, such as attempting to get an entity the client doesn't have access to - I'll refer to these as Failures.

Faults are in their nature temporal and fleeting, whether through automatic corrections made by the system itself, such as the database restarting, or fixes manually implemented by the maintainers of the system. This means that they can safely be omitted from your *GraphQL* schema because when they occur, the client has no meaningful course of action to mitigate the problem other than waiting for the issue to be fixed.

Failures, on the other hand, are persistent when attempting to get an entity the client doesn't have access to, the client does have meaningful courses of action to correct the issue, for example: not requesting the resource again or informing the user they don't have access. Because of this, it makes sense to treat these failures the same as you would a result. This means that any failures that can occur when resolving a field in a *GraphQL* server should be declared as part of a union type expressing the possible value types of a given field.

# Making Failures Type-safe
In order to make failures as results type-safe without having to add extensive logic around handling errors to the majority of functions within your *GraphQL* server, while making sure that all failure results are handled.

I suggest using a [*Result* pattern inspired by *Rust*](https://doc.rust-lang.org/rust-by-example/error/result.html). This pattern can be used to great effect in *TypeScript*. It allows us to have both the successful result typed and any possible failures to be typed.

```typescript
  getUserById(id: string): AsyncResult<UserDataModel, NotFoundError | InvalidIdentifierError> {
    return this.identifierService.user.parse(id)
      .andThenAsync((id) => this.users.load(id.value));
  }
```
This is an example of what the Result pattern looks like, when in use. This is a function on a user service that attempts to fetch a user. First, the `identifierService` parses the given string ID into the correct format for the underlying service. This results in an object of type `Result<Identifier<'User'>, InvalidIdentifierError>` if the ID is valid, the result object contains that parsed ID value. If it is invalid, it contains the `InvalidIdentifierError`. We can then call `andThenAsync` on the result object. This is essentially a map function that is only invoked if the result isn't an error and must return a new result object. The lambda function in this example returns the type `Result<UserDataModel, NotFoundError>`, this type is merged with the result type returned by the `identifierService` into the final return type of the function. 

All of this simplifies the way failures are handled because we only need to care about them when we actually want to process the errors specifically. If we don't have any way to remedy the issue on the server, it should ultimately be returned to the client as a result, and if we don't encounter an error, we can just map an intermediate result to a new result.

When generating a new result, the pattern is also trivial to use. The following is an example of how the `identifierService.user.parse(id)` method is implemented:
```typescript
  idMatcher = /(\w+)\/(0|[1-9][0-9]*)/;
  parse(id: string): Result<Identifier<Prefix>, InvalidIdentifierError> {
    const idMatch = idMatcher.exec(id);
    return idMatch && idMatch[1] === this.prefix
      ? Result.ok(new Identifier(this.prefix, parseInt(idMatch[2], 10)))
      : Result.error(new InvalidIdentifierError(id));
  }
```
Here, the string is matched against a regex and a prefix. If it doesn't match, `Result.error()` is called, and generates an error result. If it does match, `Result.ok()` is called to generate a successful result.

# Declaring the Schema
When declaring fields on the schema where the field should be resolved with the *Result* pattern described above, the field should resolve to a union type. As an example, this is how this would be declared using *GraphQL Nexus*:
```typescript
export const UserResponse = unionType({
  name: 'UserResponse',
  description: 'The type of the possible results from the user query',
  definition(t) {
    t.members(User, GraphQLNotFoundError, GraphQLInvalidIdentifierError);
    t.resolveType((root) => root.__typename);
  },
});
```
Unfortunately, there doesn't seem to be any way to add a resolver to the union type, so we can pass it a `Result` type object and let it unpack the object to the underlying value, thus we have to add a method to the `Result` class which should be called on the `Result` object returned to a field that should resolve to a union type like this.

A field that resolves to a union type like this in *GraphQL Nexus* would be declared like this:
```typescript
  t.field('user', {
    type: UserResponse,
    nullable: false,
    args: {
      id: idArg({ required: true }),
    },
    resolve: (_, { id }, ctx) => ctx.users.getUserById(id).collect(),
  }),
```
Here the `.collect()` function call unwraps the `AsyncResult<UserDataModel, NotFoundError | InvalidIdentifierError>` type object to a `Promise<UserDataModel | NotFoundError | InvalidIdentifierError>` type object.

# Summary
Treating failures as results and declaring them as return options on the field level, makes it apparent and encourages the consumer of your *GraphQL* API to handle the failures that can be encountered for any given field, and it localizes the data to where it belongs in the result data structure, instead of having it in the `errors` array on the request-response with a path to where the error originated.

# Resources
A demo project utilizing all the techniques described can be found [here](https://github.com/IT-Minds-ApS/graphql-nexus-demo).