When I was studying the Jira Clone Repository, I came across a few interesting design decisions and implementations. This post is a review about the error design pattern implemented in Jira Clone Repository. I took a vow to write clean code or atleast try.
This post provides a detailed outline of error handling
I have encountered a few common problems that this design pattern helped me solve, especially when it had to do with sending correct status code and valid error messages.
Let me provide an instance. I was working on this project that used NodeJs that should have valid, meaningful error codes and messages. There was this one particular endpoint that had a missing status code as part of the response being sent.
return res.send({ message: ‘This is an error!’});
In the above snippet unless you explicitly set status, it defaults to 200. Well that cannot be the case when it is an error.
Deployed to production and after a few hours, realized that the status code was responding with 200 even when there was an error. These status codes were really important in this project because I relied on those status codes to perform further DB operations. After some investigation, I found that it had missing status code being set like the following
/* eslint-disable max-classes-per-file */type ErrorData = { [key: string]: any };export class CustomError extends Error { constructor( public message: string, public code: string | number = "INTERNAL_ERROR", public status: number = 500, public data: ErrorData = {} ) { super(); }}export class RouteNotFoundError extends CustomError { constructor(originalUrl: string) { super(`Route '${originalUrl}' does not exist.`, "ROUTE_NOT_FOUND", 404); }}export class EntityNotFoundError extends CustomError { constructor(entityName: string) { super(`${entityName} not found.`, "ENTITY_NOT_FOUND", 404); }}export class BadUserInputError extends CustomError { constructor(errorData: ErrorData) { super("There were validation errors.", "BAD_USER_INPUT", 400, errorData); }}export class InvalidTokenError extends CustomError { constructor(message = "Authentication token is invalid.") { super(message, "INVALID_TOKEN", 401); }}
Here the CustomError class is extended by specific errors such as RouteNotFoundError, EntityNotFoundError, BadUserInputError, InvalidTokenError.
If it helps, you can create a file for each class, for example, you can put BadUserInputError into src/errors/customErrors/badUserInputError to follow the Single Responsibility Principle. Though this decision/choice is completely up to the dev.
Notice the function named findEntityorThrow. It is self explanatory, it either fetches or throws errors. And when the error is thrown, it is caught and passed to chain of middleware through next(error)
And this error is caught at src/index.ts as shown below
The above line always ensures that message, code, status, data are available and each response is consistent with this information that can be used by the client safely and reliably.
Let’s pick BadUserInputError. We can throw the following anywhere in our codebase where it is relevant
throw new BadUserInputError({ fields: errorFields });
BadUserInputError class:
export class BadUserInputError extends CustomError { constructor(errorData: ErrorData) { super("There were validation errors.", "BAD_USER_INPUT", 400, errorData); }}
CustomError class:
export class CustomError extends Error { constructor( public message: string, public code: string | number = 'INTERNAL_ERROR', public status: number = 500, public data: ErrorData = {}, ) { super();}
I have personally experienced some production issues because of inconsistency in the way errors were handled in a NodeJs based backend project. There could be other ways to handle errors better than this way, I liked this one. Hope you enjoyed reading this post.
Hey, my name is Ramu Narasinga. I study large open-source projects and create content about their codebase architecture and best practices, sharing it through articles, videos.