How to handle Errors in AWS Lambda using Typescript

avatar

Borislav Hadzhiev

Wed Apr 21 20215 min read

banner

Photo by Sarah Kilian

Updated on Wed Apr 21 2021

How to organize and handle errors using Typescript in AWS Lambda

Best Practices for Error Handling in AWS Lambda using TypeScript #

The practices for handling errors on the backend are similar regardless if you're using express.js or AWS Lambda or framework - regardless if you get a request validation error, database error, bad request error, not found error you should return the same error signature so any frontend that is calling your APIs doesn't have to be concerned about the type of error they received.

Some common error types are:

  • Request validation error - the user sent information does not satisfy the API endpoint signature. I.e. the api is expecting a field named age that is an integer and receives a field named address which is a string - status code 400
  • Database error - connection problems with the database, for example rejecting connections because of throttling - status code 500
  • Bad request error - the user provided information could not pass the necessary steps to complete the request. For example a user is signing up and enters their username but the username is already taken - status code 400
  • Not found error - the requested resource does not exist - status code 404

With the error types we often want to include information like the status code, the input field name that failed the validation check, etc. A good way to customize the error that is returned to the client is to use a try - catch. In a catch clause you normalize the error, if errors happen anywhere else in a nested try catch you massage them if you have to and rethrow the specific kind of error for that situation.

When using the default throw new Error() function in javascript we are limited to a couple of properties like message, stack, name. However often we want to provide more information. In javascript you could attach some properties on the Error, like:

const error = new Error('Invalid email or password');
error.reasons = [
  {field: 'email', message: 'Email is invalid'},
  {field: 'password', message: 'Password is required'},
];

If we now throw the error we could access the reasons property inside of the catch block.

However this would not work in typescript, because the Error object does not have a reasons property. In typescript we can't just attach some random property on the Error object.

So we want an object like Error, but one that we can extend and add some more custom properties to. This is usually a sign that we want to subclass something and add the custom properties to the subclass.

The extra properties allow us to communicate more information from the request handler to the catch block.

We can subclass the Error object like:

export abstract class CustomError extends Error {
  abstract statusCode: number;

  // taking message just to pass it to the Error constructor
  // because these messages would still be printed inside our error Logs
  constructor(message: string) {
    super(message);

    // because we are extending a built in class
    Object.setPrototypeOf(this, CustomError.prototype);
  }

  abstract serializeErrors(): {message: string; field?: string}[];
}

And the CustomError can then be extended like:

import {CustomError} from './custom-error';

export class RequestValidationError extends CustomError {
  statusCode = 400;

  // example using the `yup` validation library
  constructor(private errors: {inner: {path: string; message: string}[]}) {
    super('Invalid request parameters');

    Object.setPrototypeOf(this, RequestValidationError.prototype);
  }

  serializeErrors(): {field?: string; message: string}[] {
    return this.errors.inner.map(e => ({
      field: e.path,
      message: e.message,
    }));
  }
}

Or:

import {CustomError} from './custom-error';

export class NotFoundError extends CustomError {
  statusCode = 404;

  constructor(public message: string) {
    super('Item not found');

    Object.setPrototypeOf(this, NotFoundError.prototype);
  }

  serializeErrors(): {message: string}[] {
    return [{message: this.message}];
  }
}

CustomError is an abstract class, because it allows us to enforce methods and properties to be implemented by the classes that extend it. Abstract classes:

  • cannot be directly instantiated
  • are used to set up requirements via abstract properties/methods to be implemented on the child class that extends the abstract class
  • create a Class when translated to JavaScript, so we can use instanceof checks, as opposed to interfaces which get removed. When we translate interfaces to JavaScript all interfaces get removed, but abstract classes do NOT, so you can use instanceof checks with Abstract classes but NOT with interfaces.

In short if a class A is an abstract class and a class B extends class A, then class B has to implement all the abstract properties and methods defined on class A.

In our case all of our Error classes extend CustomError which enforces the signature of methods and properties that have to be implemented on the child classes. Where this helps us is in the catch block, since all of the Error classes implement the same signature and are instances of CustomError we can just do something like:

async function handler() {
  try {
    await somethingThatCouldError();
  } catch (error) {
    if (error instanceof CustomError) {
      return response({
        statusCode: error.statusCode,
        body: {errors: error.serializeErrors()},
      });
    }

    return response({
      statusCode: 400,
      body: {
        errors: [
          {
            message:
              "We don't know what happened, return a generic message to avoid leaking secret information, and log the error to debug later",
          },
        ],
      },
    });
  }
}

For any errors that are instanceof CustomError we know they are safe for user consumption because we have thrown them ourselves, so we just return them to the user in the form of an object of type type errors = {message: string; field?: string}[], where field is used for request validation, i.e. form fields, plus now we have the statusCode included in the Error class.

You could wrap the code from the catch block above into a function and just pass in the error to the error handler in the catch block like:

catch(error) {
  errorResponse(error)
}

For validation I use a package called yup, however it does not return errors and throws them instead, so I have to wrap the yup validation call like:

export async function validateSchema<T>(validation: Promise<T>): Promise<T> {
  try {
    return await validation;
  } catch (error) {
    console.log('YUP Error: ', error);
    throw new RequestValidationError(error);
  }
}

const {text} = await validateSchema(
  ValidationSchema.validate(body, {abortEarly: false}),
);

This allows me to throw my own errors, otherwise in the catch block where I check for the type of error I would have to include the ValidationError for yup, and I could not extract the catch block logic into a generic respondError function because any of my lambda functions could throw an error but not every needs to bundle yup and validate.

Having the consistent signature for error responses allows client apps that consume the API to not be concerned about handling different error responses. On the backend I can extend my CustomError class with any type of error that makes sense for my application and implement the statusCode property and the serializeErrors method. If the error that I get in my catch block is instanceof CustomError all I have to do is return the statusCode along with a call to serializeErrors.

Further Reading #

Join my newsletter

I'll send you 1 email a week with links to all of the articles I've written that week

Buy Me A Coffee