Tomek's blog

Error handling with Apollo Server

GraphQL is recently gaining popularity and many organizations are evaluating how they can benefit from GraphQL capabilities in their systems. There is a range of topics to consider while transitioning from a REST-based to a GraphQL-based system, one of them is error handling. In this post I’d like to share findings related to error handling with Apollo Server and in the next post I’ll cover the client side. Error handling capabilities provided by Apollo Server are described in Apollo Server documentation and Apollo blog. Not repeating information provided there, let’s focus on how those capabilities could be used in a typical use case.

Our starting point is a REST-based system divided into layers:

  • controllers layer - handling HTTP communication
  • services layer - implementing business logic
  • possibly more layers below the services layer (E.g. a model layer handling data access)

In the current system error wrapping technique is used so that a layer above only has to know how to handle errors thrown by a layer below with which it directly communicates, without having to be aware of any layers down the stack. In this setup the services layer wraps all errors that are coming from layers below and communicates them to the controllers layer that translates them into an appropriate HTTP code.

What we want to achieve is to make a transition to a GraphQL-based system without reimplementing the services layer and layers below. We can do that by either replacing REST controllers with GraphQL resolvers or placing resolvers next to controllers - in case we want the current REST-based setup to still work next to the new GraphQL setup. Either way, we want to keep the services layer and layers below intact.

Let’s group into categories errors that we have to handle:

  1. System-wide errors, not specific to one service, E.g. an error thrown when an unauthenticated user tries to access a non-public resource. This kind of error can be thrown by many different services in our system.
  2. Service-specific errors, E.g. an error thrown by addUser service when the provided email has already been registered in our system. This kind of error is limited to one service.
  3. Unexpected errors - all other errors: i/o errors, bugs in source code or project’s dependencies, etc.

Before we jump into solutions how to handle these errors let’s shortly cover what Apollo Server capabilities we can use. GraphQL-based systems convey information about errors using errors array, the array contains GraphQL errors serialized on the server side and deserialized on the client side. Type of an error is lost in the process, so Apollo Server to each error adds extensions object with code property. Value of this property conveys information of a type of the error. Apollo server provides several error classes implemented in apollo-server-errors package:

  • SyntaxError (extensions.code: GRAPHQL_PARSE_FAILED) - thrown when a server receives a malformed query: E.g. without closing parenthesis. This type of error is handled by Apollo Server itself and doesn’t reach the resolvers layer.
  • ValidationError (extensions.code: GRAPHQL_VALIDATION_FAILED) - thrown when a server receives a request non-compliant to GraphQL schema: E.g. with non-existent field names. Also handled by Apollo Server itself.
  • AuthenticationError (extensions.code: UNAUTHENTICATED) - This error type we can use in resolvers when the user in unauthenticated.
  • ForbiddenError (extensions.code: FORBIDDEN) - Can be used in resolvers when the user in unauthorized to perform a given action.
  • UserInputError - We can used it to inform that the submitted input is invalid.
  • PersistedQueryNotFoundError (extensions.code: PERSISTED_QUERY_NOT_FOUND) - only for persisted queries (more info: apollo-link-persisted-queries package).
  • PersistedQueryNotSupportedError (extensions.code: PERSISTED_QUERY_NOT_SUPPORTED) - also only for persisted queries.

All the above errors extend ApolloError class (extensions.code: INTERNAL_SERVER_ERROR), which can be used to create proprietary classes of errors.

In subsequent sections let’s see how we can map errors generated by our system to errors provided by Apollo Server.

1. Handling system-wide errors

Examples of system-wide errors may include:

  1. an error thrown when an unauthenticated user tries to access a non-public resource - let’s assume that is such a case our system throws our proprietary UserUnauthenticatedError and we would like to catch it and translate into Apollo’s AuthenticationError for every resolver that may receive it.
  2. an error thrown when the user is authenticated but tries to perform an action without having appropriate rights to do so - let’s assume that in this case the system throws our proprietary UserUnauthorizedError and we want to translate it into Apollo’s ForbiddenError for all resolvers.

One way to throw Appllo’s errors instead the proprietary ones would be to replace proprietary errors with respective Apollo’s errors in all places where these proprietary errors are thrown. But that would mean a lot of changes in existing source code and also the current REST-based setup would stop working unless we also reimplement the controllers layer. Another approach could be to put a try/catch clause in every resolver, but this would imply a lot of code repetition. Let’s try different approach and create a higher order function that takes an asynchronous function as a parameter and returns a function with the same signature as a signature of the provided function. The returned function would invoke the provided function in an asynchronous way, surround it by a try/catch block and throw Apollo’s AuthenticationError and ForbiddenError instead of our respective proprietary errors. In Javascript the higher order function could look like this:

export function translateErrors(func) {
  return async (...funcArgs) => {
    try {
      return await func(...funcArgs);
    } catch (error) {
      if (error instanceof UserUnauthenticatedError) {
        // translate UserUnauthenticatedError into Apollo AuthenticationError
        const apolloError = new AuthenticationError(error.message);
        apolloError.originalError = error;
        throw apolloError;
      } else if (error instanceof UserUnauthorizedError) {
        // translate UserUnauthorizedError into Apollo ForbiddenError
        const apolloError = new ForbiddenError(error.message);
        apolloError.originalError = error;
        throw apolloError;
      } else {
        // re-throw all other errors
        throw error;
      }
    }
  };
}

Now after passing resolvers through this function we would get all proprietary errors automatically translated. We can apply this function in the resolvers map to those resolvers that might potentially throw UserUnauthenticatedError or UserUnauthorizedError. In the following example we omit addUser and sighIn resolvers because operations associated with them are public, accessible for everyone, so they should not throw any of the errors we want to catch:

import { translateErrors } from "./errors";
import { addUserResolver } from "./addUser";
import { deleteUserResolver } from "./deleteUser";
import { getUserResolver } from "./getUser";
import { signInResolver } from "./signIn";
import { signOutResolver } from "./signOut";
import { updateUserNameResolver } from "./updateUserName";
import { updateUserEmailResolver } from "./updateUserEmail";
import { updateUserPasswordResolver } from "./updateUserPassword";
import { updateUserPhoneResolver } from "./updateUserPhone";

export const resolvers = {
  Query: {
    getUser: translateErrors(getUserResolver),
  },
  Mutation: {
    addUser: addUserResolver,
    deleteUser: translateErrors(deleteUserResolver),
    signIn: signInResolver,
    signOut: translateErrors(signOutResolver),
    updateUserName: translateErrors(updateUserNameResolver),
    updateUserEmail: translateErrors(updateUserEmailResolver),
    updateUserPassword: translateErrors(updateUserPasswordResolver),
    updateUserPhone: translateErrors(updateUserPhoneResolver),
  },
};

That would work in Javascript, but how could we achieve the same in Typescript, so that in the end we have a fully type-safe system? To annotate translateErrors function with appropriate types we have to use generics as we want to be able to pass an asynchronous function of any type to it. Let’s start with a generic constraint. The only constraint we want to impose is that a function passed as the argument should return a promise, so that we can invoke it in an asynchronous way. Apart from that we want the constraint to be as permissive as possible, the function can have any number of parameters of any type, as well as the promise that it returns can resolve to any type. We could use any type for that, but that wouldn’t be type-safe (and also our linter would complain :) ). So how can we do that in a type-safe way? First let’s start with types of parameters. They are in contravariant position, so the most permissive type here is never, as it is a subtype of, and assignable to, every type. We want to allow for any number of parameters so our parameters side of the constraint would be:

(...args: never[])

What about the return part of the constraint? We could use ReturnType<T> which is a build-in utility type provided by Typescript, but we cannot do that as the function must return a promise. So let’s build our own conditional type inferring a return type unpacked from a promise. Let’s call it UnpromisifiedReturnType<T>. In contravariant position we are going to use any number of arguments of type never and in covariant position we are going to use type unknown, since anything is assignable to unknown, but unknown is not assignable to anything:

type UnpromisifiedReturnType<
  T extends (...args: never[]) => Promise<unknown>
> = T extends (...args: never[]) => Promise<infer R> ? R : never;

In the example above when Typescript compiler is unable to infer type then returns never, and since nothing is assignable to never no type will be able to be assigned to it and the compiler will warn us about an improper assignment. Now we can build our generic constraint:

function translateErrors<
  T extends (...args: never[]) => Promise<UnpromisifiedReturnType<T>>
>

Next let’s annotate return type of translateErrors function. To annotate it we can use another built-in utility type provided by Typescript: Parameters<T>. The returned function should take the same types of parameters as the provided function (Parameters<T>) and should return a promise of unpromisified return type (UnpromisifiedReturnType<T>) With use of it return type of translateErrors function would be:

type AsyncFunction<T extends (...args: never[]) => Promise<unknown>> = (
  ...args: Parameters<T>
) => Promise<UnpromisifiedReturnType<T>>;

Ok, so now let’s put all the blocks together:

type UnpromisifiedReturnType<
  T extends (...args: never[]) => Promise<unknown>
> = T extends (...args: never[]) => Promise<infer R> ? R : never;

type AsyncFunction<T extends (...args: never[]) => Promise<unknown>> = (
  ...args: Parameters<T>
) => Promise<UnpromisifiedReturnType<T>>;

export function translateErrors<
  T extends (...args: never[]) => Promise<UnpromisifiedReturnType<T>>
>(func: T): AsyncFunction<T> {
  return async (
    ...funcArgs: Parameters<T>
  ): Promise<UnpromisifiedReturnType<T>> => {
    try {
      return await func(...funcArgs);
    } catch (error) {
      if (error instanceof UserUnauthenticatedError) {
        // translate UserUnauthenticatedError into Apollo's AuthenticationError
        const apolloError = new AuthenticationError(error.message);
        apolloError.originalError = error;
        throw apolloError;
      } else if (error instanceof UserUnauthorizedError) {
        // translate UserUnauthorizedError into Apollo's ForbiddenError
        const apolloError = new ForbiddenError(error.message);
        apolloError.originalError = error;
        throw apolloError;
      } else {
        // re-throw all other errors
        throw error;
      }
    }
  };
}

As a result we have a function that takes an asynchronous function and returns another asynchronous function of exactly the same signature as the one that was passed in. The returned function invokes the passed in function in an asynchronous way, catches proprietary errors and throws Apollo’s errors instead.

Now we have to apply that function to resolvers. We might be tempted to apply it to all resolvers in a loop, for example by using Object.entries or for/in with hasOwnProperty. But there is an important caveat: by doing so we loose type safety as Typescript while looping through an object casts types of all values to any. What at most we could do is to invoke Object.entries with type variable set to the most permissive asynchronous function, E.g. like this:

Object.values(resolvers).forEach(resolverGroup => {
  Object.entries<(...args: never[]) => Promise<unknown>>(resolverGroup).forEach(
    entry => {
      resolverGroup[entry[0]] = translateErrors(entry[1]);
    },
  );
});

Then the compiler would at least catch if we try to pass a synchronous function instead of an asynchronous one, but still would not be able to warn about arguments or a return type not conforming to our GraphQL schema. So let’s apply the function selectively, but before we do that let’s add one more tool to our system: graphql code generator. It’s a tool that can generate Typescript types based on GraphQL schema. Out of generated types we are going to use MutationResolvers, QueryResolvers and ResolversObject in our resolvers map. Now let’s see how the map looks like in Typescript:

import {
  MutationResolvers,
  QueryResolvers,
  ResolversObject,
} from "../graphql/generated";
import { translateErrors } from "./errors";
import { addUserResolver } from "./addUser";
import { deleteUserResolver } from "./deleteUser";
import { getUserResolver } from "./getUser";
import { signInResolver } from "./signIn";
import { signOutResolver } from "./signOut";
import { updateUserNameResolver } from "./updateUserName";
import { updateUserEmailResolver } from "./updateUserEmail";
import { updateUserPasswordResolver } from "./updateUserPassword";
import { updateUserPhoneResolver } from "./updateUserPhone";

type Resolvers = ResolversObject<{
  Query: QueryResolvers;
  Mutation: MutationResolvers;
}>;

export const resolvers: Resolvers = {
  Query: {
    getUser: translateErrors(getUserResolver),
  },
  Mutation: {
    addUser: addUserResolver,
    deleteUser: translateErrors(deleteUserResolver),
    signIn: signInResolver,
    signOut: translateErrors(signOutResolver),
    updateUserName: translateErrors(updateUserNameResolver),
    updateUserEmail: translateErrors(updateUserEmailResolver),
    updateUserPassword: translateErrors(updateUserPasswordResolver),
    updateUserPhone: translateErrors(updateUserPhoneResolver),
  },
};

In this case translateErrors function translates only two errors but we can expand it to cover more types of errors based on our system’s needs. And also in case the underlying system is not so uniform we can provide more than one translateErrors functions, each suited to unique needs of part of the system. To any resolver several such functions can be applied. Also some of them can return non-asynchronous functions in case part of the system works in a synchronous way.

Let’s wrap up, with this setup we don’t have to make any modifications to our underlying system, nor do we have to duplicate try/catch blocks for system-wide errors in every resolver. And by using types generated by graphql code generator we gain full type safety of the system: when we forget to cover some of queries or mutations with resolvers or use a resolver with incorrect set of arguments or return type then it is going to be caught in the compilation phase.

Now let’s see how we can handle service-specific errors.

2. Handling service-specific errors

Service-specific errors might include for example:

  • EmailExistsError thrown by addUser service when the provided email is already registered in the system,
  • InvalidCredentialsError thrown by signIn service when the user tries to sign in with invalid email or password,
  • etc.

Errors like these are a result of users’ actions that don’t pass server-side validation. Apollo Server provides special error type for handling errors like these: UserInputError. On instantiation its constructor function takes two arguments: required message and optional properties:

export class UserInputError extends ApolloError {
  constructor(message: string, properties?: Record<string, any>) {
    super(message, 'BAD_USER_INPUT', properties);

    Object.defineProperty(this, 'name', { value: 'UserInputError' });
  }
}

To translate a service-specific error into UserInputError we can simply catch it in the resolver that invokes the service and use properties argument to augment error details. An issue with this approach is that errors are not part of GraphQL schema so any changes to error details have to be synchronized in all places where the error is consumed on the client side. Let’s use Typescript compiler to check it for us and provide type safety also in this area.

First let’s make properties argument mandatory by extending UserInputError:

export class InvalidUserInputError extends UserInputError {
  constructor(message: string, properties: UserInputErrorProperties) {
    super(message, properties);
  }
}

Now we have to decide what shape properties object is going to have. For example we can decide that every properties object will contain:

  • operation property conveying information in which operation the error occurred
  • codes property conveying information what type/types of errors occurred
  • other optional properties, for example communicating the actual error message to be displayed in case of a server-side i18n

So every properties object would have to adhere to the following shape:

interface ErrorProperties {
  operation: UserInputOperation;
  codes: Array<UserInputErrorCode>;
}

Let’s use string enums to assign string literals for three exemplary operations:

export enum UserInputOperation {
  AddUser = "ADD_USER",
  SighIn = "SIGN_IN",
  UpdateUserEmail = "UPDATE_USER_EMAIL",
}

and three exemplary error codes:

export enum UserInputErrorCode {
  EmailExists = "EMAIL_EXISTS",
  InvalidCredentials = "INVALID_CREDENTIALS",
  InvalidPassword = "INVALID_PASSWORD",
}

Next we export UserInputOperation and UserInputErrorCode as we’ll be using them in resolvers. Now we have to decide which operations can send which error codes, for example:

Operation ADD_USER can send error code EMAIL_EXISTS:

interface AddUserErrorProperties extends ErrorProperties {
  operation: UserInputOperation.AddUser;
  codes: Array<UserInputErrorCode.EmailExists>;
}

Operation SIGN_IN can send error code INVALID_CREDENTIALS:

interface SignInErrorProperties extends ErrorProperties {
  operation: UserInputOperation.SighIn;
  codes: Array<UserInputErrorCode.InvalidCredentials>;
}

And operation UPDATE_USER_EMAIL can send error codes EMAIL_EXISTS and INVALID_PASSWORD:

interface UpdateUserEmailErrorProperties extends ErrorProperties {
  operation: UserInputOperation.UpdateUserEmail;
  codes: Array<
    UserInputErrorCode.EmailExists | UserInputErrorCode.InvalidPassword
  >;
}

If any operation needs additional properties for handling an error on the client side then we can add them next to operation and codes properties. In resolvers we can import these interfaces either individually or we can group them into an union and use Typescript’s discriminated union concept. If we do that then operation property will become the discriminant of the union and after selecting appropriate operation the union type will get narrowed down to a type specific to this operation. Let’s do that and export only the union type:

export type UserInputErrorProperties =
  | AddUserErrorProperties
  | SignInErrorProperties
  | UpdateUserEmailErrorProperties;

Now let’s use the union in an exemplary resolver, along with types generated by graphql code generator:

export async function updateUserEmailResolver(
  parent: {},
  args: MutationUpdateUserEmailArgs,
  ctx: ResolverContext,
): Promise<UpdateUserMutationResponse> {
  try {
    const updatedUser = await updateEmail(ctx, {
      email: args.email,
      userId: args.userId,
      currentPassword: args.currentPassword,
    });
    const mutationResponse: UpdateUserMutationResponse = {
      isSuccess: true,
      message: "User's email updated successfully",
      user: updatedUser,
    };
    return mutationResponse;
  } catch (error) {
    if (error instanceof InvalidPasswordError) {
      const properties: UserInputErrorProperties = {
        operation: UserInputOperation.UpdateUserEmail,
        codes: [UserInputErrorCode.InvalidPassword],
      };
      const apolloError = new InvalidUserInputError(
        "Failed to update user's email, invalid credentials",
        properties,
      );
      apolloError.originalError = error;
      throw apolloError;
    } else if (error instanceof EmailExistsError) {
      const properties: UserInputErrorProperties = {
        operation: UserInputOperation.UpdateUserEmail,
        codes: [UserInputErrorCode.EmailExists],
      };
      const apolloError = new InvalidUserInputError(
        "Failed to update user's email, email already registered",
        properties,
      );
      apolloError.originalError = error;
      throw apolloError;
    } else {
      throw error;
    }
  }
}

When updateEmail service completes successfully then we build mutationResponse object and return it. But when it throws InvalidPasswordError or EmailExistsError then it is translated into Apollo’s UserInputError with an appropriate properties object. All other errors are rethrown. In this setup when we select an operation correctly then Typescript’s compiler will not allow us to use error code other than those defined for this operation, and if we add other properties to the interface related to this operation then Typescript compiler will also display an error if we forget to add them to properties object created in the resolver.

Even bigger benefit we would gain when we exclude UserInputErrorProperties and related enums to a separate project, common for both client and server side. Then after using it also on the client side we could gain full type safety across the system: in case of any changes in error handling for an operation Typescript compiler would show us where the operation is used and the system would not compile until changes are synchronized in all these places. We’ll see how we could do that in the next post.

Now let’s handle the last category of errors - all other, unexpected errors.

3. Handling unexpected errors

Examples of unexpected errors might include:

  • i/o errors related to database/filesystem/external api access issues
  • errors resulted from bugs in source code or dependencies of the project
  • etc.

When no layer of our system knows how to handle an error then it is rethrown up layer by layer. And as a sidenote, it would be a bad practice to silently suppress unexpected errors in a layer that cannot properly handle them. Before the unexpected error reaches egress we can use one more capability of Apollo Server: masking and logging errors. On instantiation Apollo Server takes config object as the constructor parameter and one of properties of this object is formatError property to which we can assign a function. If the function is provided then every error is passed through it before being transmitted outside. Let’s see how the function could look like:

import { GraphQLError } from "graphql";
import {
  ApolloError,
  AuthenticationError,
  ForbiddenError,
  UserInputError,
} from "apollo-server-express";
import { logger } from "./logger";

const path = "errors/";

export function formatError(error: GraphQLError): GraphQLError {
  const functionPath = path + formatError.name;

  const originalError = error.originalError;
  if (originalError instanceof ApolloError) {
    if (
      originalError instanceof AuthenticationError ||
      originalError instanceof ForbiddenError ||
      originalError instanceof UserInputError
    ) {
      logger.verbose(functionPath, JSON.stringify(error));
    }
  } else {
    logger.error(functionPath, JSON.stringify(error));
  }
  // Do not send stacktrace to clients:
  if (error.extensions?.exception?.stacktrace !== undefined) {
    error.extensions.exception.stacktrace = undefined;
  }
  return error;
}

In this function we filter out all errors that are related to user’s invalid actions and log all other errors as actual errors that need attention. We can also clear the stacktrace so that it is not transmitted to the client application, as we don’t want to expose the internal structure of our system outside. There are also other ways to disable the stacktrace in production. After the unexpected error is processed it is transmitted outside with extensions.code set to INTERNAL_SERVER_ERROR. Such an error should be subsequently properly handled in a client application’s UI, we’ll see how to do that in the next post.

Wrapping up, handling unexpected errors on the server side boils down to properly logging/reporting them - as solving issues that caused them usually requires human intervention.

Summary

Let’s summarize, as a result of implemented changes we have a system that handles system-wide, service-specific and unexpected errors and translates them into Apollo’s errors in a type-safe way with Typescript. But most importantly, all changes have been implemented in the resolvers layer with no modifications required to layers below. As a result, apart from added GraphQL capabilities, the system works as before and can still handle REST requests, allowing for easier transition from a REST-based to a GraphQL-based system.

Looking forward to your comments and questions in this reddit thread.

In the next post let’s see how we can handle errors sent by Apollo Server on the client side.