In this article I will show you how to use TypeScript to write consistent and DRY error handling logic using the concept of abstract classes.
This post follows well along with my video tutorial on YouTube. All the code and ideas mentioned here can be found on my github repository for you as a boilerplate to copy and paste.
The GitHub repository for this tutorial can be found here: https://github.com/mmaksi/nodejs-error-handling
If you are not familiar with the idea of error handling in JavaScript, check out my other blog first: Error Handling in JavaScript - The Complete Guide
For this tutorial I am going to use Express.js as a Node.js framework since it's the most popular and most adopted in the industry.
Error Handling By a Middleware
What is a Middleware
A middleware in brief is simply a function that intercepts the request in order to apply certain logic before it reaches its destination: the request handler. Not only they intecept the request, but also the response. After the request handler does its job when receiving the request, the response also passes again through the same middleware that intercepted the request before reaching its destination: the client or web browser.
Error handling in Express.js happens by simply defining a middleware (function) that accepts 4 parameters: error
, request
, response
, next
as follows:
and to tell Express.js that this is an error handling middleware, inside index.ts
we mount it on the app after we define all of our routers and request handlers as follows:
Handling Synchronous Errors in Express.js
The error handling middleware we defined earlier catches any error thrown in our application as the examples below:
And we can define any logic for error handling inside the middleware and it will work fine.
Handling Asynchronous Errors in Express.js
But if we decided to throw
an error inside an asynchronous request handler, which is the case in most of the time since we deal with database and APIs, the error handler will not work and our application will crash without catching any errors!
To handle asynchronous errors in Express.js, we must call the next
function as follows:
And now our application will work fine!
So far so good, but there's a problem. Errors can be thrown for different reasons. We have database erros, general bad request errors, authentication errors, not found errors and the list goes on and on.. No where in our application we can differentiate between the different types of errors. So it's a good idea to define custom errors like DatabaseError
or AuthenticationError
.
To create custom errors like that, we have 2 options.
The first is to extend the
Error
built-in constructor and apply the same custom logic in every custom error.The magical line of code in the image above is added just when we need to extend the Error constructor in TS and to know why it was and added and what it does, check out this: javascript - Typescript - Extending Error class - Stack Overflow
and now we can instantiate the custom
DatabaseError
error class (and other similar custom error classes) and differentiate this error from the generalError
in the error handling middleware as follows:There are 2 problems in this approach. The first one is that we have to check for every custom error so we know what status code and what json to return, and that makes our code very repetitive.
The second problem is that any developer can come now and define a custom error like
NotFoundError
that defines its own blueprint, its own properties and its own methods without complying to the same names defined in other custom error classes, and TypeScript will not complain about this code inconsistency. E.g: definingserializeError()
orserializee()
instead ofserialize()
, and that makes our code inconsistent and not DRY.So far we are handling the custom errors like in this diagram:
To solve the 2 problems I mentioned earlier:
Repetitive code
Inconsistent code
I am going to follow a second approach to handle the custom errors.
The second approach is to define an
abstract class
. An abstract class is a class that acts as a blueprint to tell other classes how they should be created. Abstract classes also cannot be instantiated.To understand the concept of abstract classes, think of when you want to create buildings. We can define a blueprint for every building, or we can define a single blueprint for multiple buildings. The former is the first approach discussed earlier, the latter is what abstract classes is all about - they keep our code consistent and DRY. Let me show you how to do it.
Using Abstract Classes To Create Consistent And DRY Error Handling Logic
I'm going to define an abstract class called CustomError
:
Now in every custom error we are going to extend this CustomError
instead of the built-in Error
:
TypeScriot now will throw errors inside any error class that extends the CustomError
until the developer implements at least a property called statusCode
and a method called serialize()
. If the developer defines statuscode
instead of statusCode
, TypeScript will throw errors. This solves the problem of code inconsistency. Let's see how this approach also solves the problem of repetitive code.
In the error handler middleware, instead of checking if the error thrown is an instance of DatabaseError
or AuthenticationError
or something else, we simply can check if the error is an instance of CustomError
and simply return the status code and the json object:
I know this seems like magic, but let me show you what we've done in a simple diagram:
That's it! You've successfully learned how to use abstract classes in TypeScript to handle error in Node.js and Express.js like a pro. Any questions? Leave comments for me down below.