How to handle Errors in AWS Lambda using Typescript

avatar
Borislav Hadzhiev

Last updated: Jan 14, 2023
5 min

banner

# 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 to the Error.

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 as follows:

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 a 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 implements 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.

# Additional Resources

You can learn more about the related topics by checking out the following tutorials:

I wrote a book in which I share everything I know about how to become a better, more efficient programmer.
book cover
You can use the search field on my Home Page to filter through all of my articles.