Don't ignore Error handling.

The core stuff that most of the tutorials on the internet ignore.

Architecture
Tutorial

When it comes to creating full-stack applications that require high up-time and high availability, managing and handling errors properly becomes more of a necessity. If your application does not have a good error handling architecture then its already doomed.

Error handling is more important than it seems because, first that it helps you, yes you, the developer to debug your application much easily, and second that properly handled errors gives you a sense of peace of mind that if something messes up you have a fallback mechanism that'll save your day.

What and why of "Errors"

Why do we need Errors in the first place? Applications are designed and always kept maintained so that they are less error prone, however discrepancies occur.

Another thing to note here is that an "error" must not always be an "exception". The difference between the terms is really important. In layman words consider an "exception" to be something that is application related, say something wrong in the code. Whereas, an error might be much broader, say any invalid input by the user. It won't crash or break your application but still is an error.

Errors while creating errors

Let's think about a typical scenario, you have an application that is built with modern (M/P)ERN Stack (databases are your choice). You would have several routes and several controllers handling those routes for you. However you know this very well that things can break, say you have a route that checks if a user entered a valid string.

1import { Request, Response } from 'express';
2
3const route = (req: Request, res: Response) => {
4 const { text }: { text: string } = req.body;
5
6 if (text.length.trim() === 0) {
7 return res.status(401).send('Not valid!');
8 }
9
10 return res.status(200).send("Valid!");
11 }
12}

The code above seems quite good and to be honest will work; however, there is a flaw. The issue is the 'send' part. Especially where we send "Not Valid" is not in a correct or more importantly in a predefined format. Having a predefined format helps a lot, especially when creating the frontend for your application, it will save you a headache.

Now think that you have several other routes and they are creating errors in their own fashion. This is a horrible approach since every route will have its own errors and the frontend will have to implement these errors separately. Which makes developing frontend more painful since you'll have to modify the logic every time you get data from a certain endpoint.

A better approach!

Worry not my friends because there is a better way to deal with this. For this I'll be using Expressjs, which is a Nodejs framework, however this can be implemented it any framework or language of your choice. As long as you read their documentation properly and take your time to dig a bit deeper.

Expressjs provides a Error handling middlware, the middlware is possibly the only middlware in express that takes 4 arguments.

1import { Request, Response, NextFunction } from 'express';
2
3const errorHandler = (err: Error, req: Request, res: Response, next: NextFunction) => {
4 // Every request that throws an error will pass through this middleware
5 // as long as the middleware is plugged into your application it will do its job
6 // by default the next function is called automatically so there is no need of calling it
7};
8
9export default errorHandler;

Now what we can do is create the function above and pass it at the very bottom of your express application as a global error handler.

1const app = express();
2
3// All the other routes and everything
4// after that plug in the handler
5
6app.use(errorHandler);
7
8app.listen(5000, () => {
9 console.log('Listening on port 5000');
10});

However its not fully ready yet. Remember that this middlware can't do anything on its own we need to configure it first. Now first we need to define the structure of the errors that we'll be throwing. This serializes the errors structure across entire application and helps us be consistent with it. For this we'll be using a simple object that will contain an array of errors with a message and optional field property.

1// This is what the error will look like
2{
3 [
4 { message: 'This is the first error' },
5 { message: 'This error occured in a field, probably invalid input', field: 'name' },
6 ];
7}

Now the good part with the structure above is that you can loop over the array and display the errors one-by-one, this also helps the frontend since it knows no matter what the errors, they will have that same structure so we need to create just one component and reuse it everywhere.

Now that won't just work out by itself, we need to setup some Errors and by that I mean classes. To give you can idea of how we can create errors in express with this approach and how Express will handle it for us, take a look down below.

1const controller = (req: Request, res: Response) => {
2 // Some logic for the controller and say it fails
3 if (fails) {
4 throw new Error('Something broke!'); // This error will be caught by express
5 }
6};

The error thrown will be caught by express and will be passed to our globalErrorHandler. Now there are few things that we need to keep in mind, first that errors can be of two types, either they can be errors generated by the code say something failed; good old user input type. However there are uncaught exceptions as well. Errors that we have no idea of. So we also need to make sure that we don't leak any details.

1import { Request, Response, NextFunction } from 'express';
2
3const errorHandler = (err: Error, req: Request, res: Response, next: NextFunction) => {
4 res.status(500).json({ errors: [{ message: "Something broke! We're on it." }] });
5};

The statement above will get fired regardless of what error. It is more of a generic message that is sent back to the frontend if something happens which is unhandled. Let's start by creating a "customError" class. We're creating this as an abstract class so that we can inherit this class (we're using TypeScript can be done in js without abstract keyword however responsibility is yours) and create errors.

1// this class must be extended whenever creating a custom error
2export default abstract class CustomError extends Error {
3 abstract statusCode: number; // Extra stuff I added you can create more but be consistent and logical
4
5 constructor(message: string) {
6 super(message);
7 Object.setPrototypeOf(this, CustomError.prototype);
8 }
9
10 abstract serializeErrors(): { message: string; field?: string }[];
11}

Above we're extending the Error object because, first it must be an Error to be recognized as an error to Express (remember we set the type of err as Error in our error handler), and second we're using an abstract class here. Abstract classes can only be inherited they can't be initialized (yes I am not using an interface). Also note that we have a method called "serializeErrors" that emmits out the structure of our error, this will ensure that every error class we create will have the same error structure.

Let's create our very first error and see how can we throw it.

1import CustomError from './CustomError';
2
3export default class BadRequestError extends CustomError {
4 statusCode = 400;
5
6 constructor(public message: string) {
7 super(message);
8 Object.setPrototypeOf(this, BadRequestError.prototype);
9 }
10
11 serializeErrors() {
12 return [{ message: this.message }];
13 }
14}

Again here we're creating a BadRequestError out of CustomError. And this class also has to implement the serializeErrors method and make sure that it returns the exact same structure of the error. Now this can be used inside of our code. However we need to add one more thing before we start using it.

1import { Request, Response, NextFunction } from 'express';
2import CustomError from './customError';
3
4const errorHandler = (err: Error, req: Request, res: Response, next: NextFunction) => {
5 if (err instanceof CustomError) {
6 return res.status(err.statusCode).send({ errors: err.serializeErrors() });
7 }
8
9 res.status(500).json({ errors: [{ message: "Something broke! We're on it." }] });
10};

What we did above is to verify if the error thrown was an error which is derived from our CustomError class. And if it was then we know that it must also contain a serializeErrors method that will return the errors in just the way we want them. Also note that I am using the statusCode here that I made a attribute in my CustomError class. Also note that we're returning it because we don't want the flow to continue.

Now we're free to use this as we like!

1const controller = (req: Request, res: Response) => {
2 // Some logic for the controller and say it fails
3 if (fails) {
4 throw new BadRequestError('Invalid input'); // Yay
5 }
6};

You might be pretty happy that this worked or maybe not?! Because you tried throwing it inside of a async block (if you haven't, try it). You'll see that it just doesn't work.

The reason for that is actually framework dependent, for express if the error is thrown inside of a controller that returns a Promise (remember async functions don't directly resolve but they return a promise which WILL resolve) the error must be thrown inside the next function that it provides. For that we'll just use a library called "express-async-errors" just import it at top level of your app and you're done!

Express Async Errors on NPM

1// At the top of your app.ts or index.ts (must be your application entry point)
2import 'express-async-errors';

And there you go! Now you have a better error handling architecture ready. And this makes working on the frontend 10x easier. Liked this? Then make sure to share this.