Last updated: Jan 14, 2023
Reading time·5 min
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:
age
that is an
integer and receives a field named address
which is a string - status code
400With 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:
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
.
You can learn more about the related topics by checking out the following tutorials: