Communicating errors is one of the most important feature almost every API MUST handle. Some backend frameworks may have inbuilt error types and schemas ready to use out of the box, some may not.
A standardized schema has to be mutually agreed upon by the different consumers and internal services for an API. Even if there’s a simple way to implement it using say, Framework-X, its not necessary all other services will be built using the same framework.
A degree of customization in error communication is hence always essential in any backend architecture.
While learning about usage of Typescript in Nodejs, I learnt how we can apply OOPs principles to devise a pattern to deal with errors. This can be done using Javascript too, but using Typescript guides us with smart autosuggestions wherever we create instances.
This advantage really pays off the initial efforts we put to build such custom thing.
[
{
"message" : "some text1 here",
"field" : "some field1 here, optional"
},
{
"message" : "some text2 here",
"field" : "some field2 here, optional"
}
// ....
]
export abstract class CustomError extends Error {
abstract statusCode: number;
constructor(message: string) {
super(message); // pass this to native 'Error' class in JS
Object.setPrototypeOf(this, CustomError.prototype);
}
// returns array of type (message, field?)[]
abstract serializeErrors(): { message: string; field?: string }[];
}
import { CustomError } from './custom-error';
// DB related error
export class DatabaseConnectionError extends CustomError {
statusCode = 500;
reason = 'Error connecting to database';
constructor() {
super('Error connecting to db');
Object.setPrototypeOf(this, DatabaseConnectionError.prototype);
}
serializeErrors() {
return [{ message: this.reason }];
}
}
// Not authorized error
export class NotAuthorizedError extends CustomError {
statusCode = 401;
constructor() {
super('Not Authorized');
Object.setPrototypeOf(this, NotAuthorizedError.prototype);
}
serializeErrors() {
return [{ message: 'Not authorized' }];
}
}
Since the base CustomError and derived class are now defined, the nice and final step remains.
import { Request, Response, NextFunction } from "express";
import { CustomError } from "../errors/custom-error";
export const errorHandler = (
err: Error,
req: Request,
res: Response,
next: NextFunction
) => {
// if this is user-defined recognized error, as 'CustomError ' class
// we can use its methods and attributes to serialize return msg
if (err instanceof CustomError) {
return res.status(err.statusCode).send({ errors: err.serializeErrors() });
}
// unrecognized error; send generic error message
console.error(err);
res.status(400).send({
errors: [{ message: "Something went wrong" }],
});
};
This first seems little extra work for sure, especially for such simple errors, however as the complexity grows, we wont have to remember what our error message are supposed to look like, their properties etc.
class NewError extends CustomError
and let typescript intellisense guide the way to you. 😌Coming soon...