DEV Community

Cover image for Deftly🦅 Handle exceptions in Node/Express Application
Smitter
Smitter

Posted on

Deftly🦅 Handle exceptions in Node/Express Application

Welcome to part 4. Follow along this series to build a 3-tier Login System implementing core features required of an effective and usable authentication system.

Summary: In this tutorial, you will learn to create user-defined exceptions and handle these exceptions (or other unexpected events) as they occur through proper error handling in express.

Note 🔔:

If you would like to jump ahead to the finally built authentication system, the complete Git Repo can be found on Github✩.

With how Javascript engine works, when a runtime error occurs, execution will stop and an error is generated. In technical parlance, a runtime error causes new Error object to be created and thrown. In such an event, the rest of code is not executed.

Well, that is how Javascripts handles itself when you write bad code. Likewise, the application you are building needs to handle itself when it receives bad input. Or when other unexpected events occur, including a runtime error.

For example, division by zero is not an error in Javascript. But logic in your application requires that it should be. The throw keyword allows you to stop javascript execution whilst generating a custom defined exception. As you generate exception, you should handle it and communicate back to the user with succinct information about what went wrong.

Table of Contents

  1. What we will do
    1. Create custom error constructors
    2. Create error/exception handlers
    3. Configure error handlers in express application
    4. Error Handling in action
  2. Final thoughts

Outline of what we will do:

  1. Create custom error constructors.
  2. Create error/exception handlers.
  3. Configure express application to use custom error handlers.
  4. Error Handling in action.

In an error flow control, we will throw using a custom error constructor, to generate a user-defined exception. The generated exception will be processed in error handler.

Directory structure

📂server/
└── 📂src/
    ├── 📄index.js
    └── 📂config/
+       ├── 📂errors/
+       │   ├── 📄AuthorizationError.js
+       │   └── 📄CustomError.js
+       └── 📂exceptionHandlers/
+           └── 📄handler.js
Enter fullscreen mode Exit fullscreen mode

This is the structure in regard to the src directory that contains our code. We will update these files with code to provide the logic we want to achieve. You can always double check to create these files in their correct locations.

1. Create custom error constructors

Certainly you may be familiar with the in-built Error constructor, i.e new Error(), which actually creates an Error object.

Let's create custom error constructor which is a superset of the in-built Error, by which we will be able to add more properties to the created Error Object we will need later.

First, open the file: CustomError.js. According to the directory structure for this tutorial(refer above), it should be at the path: server/src/config/errors/. Create the constituent directories if you do not have.

Inside the opened file, paste in the following code:

class CustomError extends Error {
    /**
     * Custom Error Constructor
     * @param {any} [message] - Optional error payload
     * @param {number} [statusCode] - Optional error http status code
     * @param {string} [feedback=""] - Optional feedback message you want to provide
     */
    constructor(message, statusCode, feedback = "") {
        super(message);
        this.name = "CustomError";
        this.status = statusCode;
        this.cause = message;
        this.feedback = String(feedback);
    }
}

module.exports = CustomError;
Enter fullscreen mode Exit fullscreen mode

The snippet above simply creates a class that extends base Error class that is in-built to javascript. We define a constructor that passes message as first argument to the base class(By calling super(message)), and adds additional properties to the result Error Object when the class is instantiated.

To cause a user-defined exception in our application, we will throw CustomError(). Arguments can be passed to define properties on created Error object.

Second, open/create the file: AuthorizationError.js. Expected to be at the path: server/src/config/errors/(refer above).

Paste the following code in the file:

const CustomError = require("./CustomError");

class AuthorizationError extends CustomError {
    /**
     * Authorization Error Constructor
     * @param {any} [message] - Error payload
     * @param {number} [statusCode] - Status code. Defaults to `401`
     * @param {string} [feedback=""] - Feedback message
     * @param {object} [authParams] - Authorization Parameters to set in `WWW-Authenticate` header
     */
    constructor(message, statusCode, feedback, authParams) {
        super(message, statusCode || 401, feedback); // Call parent constructor with args
        this.authorizationError = true;
        this.authParams = authParams || {};
        this.authHeaders = {
            "WWW-Authenticate": `Bearer ${this.#stringifyAuthParams()}`,
        };
    }

    // Private Method to convert object `key: value` to string `key=value`
    #stringifyAuthParams() {
        let str = "";

        let { realm, ...others } = this.authParams;

        realm = realm ? realm : "apps";

        str = `realm=${realm}`;

        const otherParams = Object.keys(others);
        if (otherParams.length < 1) return str;

        otherParams.forEach((authParam, index, array) => {
            // Delete other `realm(s)` if exists
            if (authParam.toLowerCase() === "realm") {
                delete others[authParam];
            }

            let comma = ",";
            // If is last Item then no comma
            if (array.length - 1 === index) comma = "";

            str = str + ` ${authParam}=${this.authParams[authParam]}${comma}`;
        });

        return str;
    }
}

module.exports = AuthorizationError;
Enter fullscreen mode Exit fullscreen mode

AuthorizationError is an error intended to be generated due to insufficient authentication.

It is extends the CustomError class adding extra properties as well. Majorly, the constructor for this error class has a statusCode that defaults to 401 which is passed to base class(CustomError) when calling super().

Also, we have defined #stringifyAuthParams() private method to convert authParams object parameter to a list of comma separated strings of format key=value. The formatted string is used to generate value for the authheaders property.

New to private methods in javascript classes? Read more here.

Error object created using this class will be processed by error handler to produce a 401 - Unauthorized Error response that observes the web specifications, which require a response due to failed authentication include a WWW-Authenticate header. Learn more here.

2. Create error/exception handlers

Here we will create handlers for unexpected events that occur in our application. These handlers are important to prevent a running program from shutting down unceremoniously to its users. Furthermore they provide concise information to the user about what went wrong.

We will create two error handlers:

  1. 404 case handler i.e handler called when http request on a path that does not exist hits our server.
  2. General exception handler i.e handler called when exceptions are generated from anywhere in our application.

Open the file: handler.js. According to the directory structure for this tutorial(refer above), it should be at the path: server/src/config/exceptionHandlers/. Create the constituent directories if you do not have.

Paste the following code inside the file:

// 404 Error Handler
function LostErrorHandler(req, res, next) {
    res.status(404);

    res.json({
        error: "Resource not found",
    });
}

// Exception Handler
function AppErrorHandler(err, req, res, next) {
    res.status(err.status || 500);

    if (err.authorizationError === true) {
        // Sets headers available in Authorization Error object
        res.set(err.authHeaders);
    }

    // `cause` is a custom property on error object
    // that may contain any data type
    const error = err?.cause || err?.message;
    const providedFeedback = err?.feedback;

    // respond with error and conditionally include feedback if provided
    res.json({
        error,
        ...(providedFeedback && { feedback: providedFeedback }),
    });
}

module.exports = { LostErrorHandler, AppErrorHandler };
Enter fullscreen mode Exit fullscreen mode

We have defined two functions here taking the signature of express middleware.

The first one, LostErrorHandler() function is the 404 case handler. Importantly, it sets a 404 status code on the response(res).

And the second one, AppErrorHandler() function has 4 parameters which distinguishes it as an express error middleware. This is the General exception handler.

Properties are available on the err parameter of the AppErrorHandler() function depending on error constructor that created the Error object. For example we see this line:

// ...
if (err.authorizationError === true) {
    res.set(err.authHeaders);
}
// ...
Enter fullscreen mode Exit fullscreen mode

We know err is an authorization error object when we throw an exception using the custom AuthorizationError constructor, like:

throw new AuthorizationError();
Enter fullscreen mode Exit fullscreen mode

This will create an Error object that contains authorizationError property set to true. Also includes authHeaders property e.t.c.

3. Configure express application to use custom error handlers.

The last step is to configure the express application to use error handlers we have created. For this, open index.js which is the entrypoint to our application. This file should be located at server/src/.

Add the following code to this file:

const express = require("express");

const {
    AppErrorHandler,
    LostErrorHandler,
} = require("./config/exceptionHandlers/handler.js");

/* 
  1. INITIALIZE EXPRESS APPLICATION 🏁
*/
const app = express();
const PORT = process.env.PORT || 8000;

/* 
  2. APPLICATION MIDDLEWARES AND CUSTOMIZATIONS 🪛
*/
// ...

/* 
  3. APPLICATION ROUTES 🛣️
*/
// Test route
app.get("/", function (req, res) {
    res.send("Hello Welcome to API🙃 !!");
});

/* 
  4. APPLICATION ERROR HANDLING 🚔
*/
// Handle unregistered route for all HTTP Methods
app.all("*", function (req, res, next) {
    // Forward to next closest middleware
    next();
});
app.use(LostErrorHandler); // 404 error handler middleware
app.use(AppErrorHandler); // General app error handler

/* 
  5. APPLICATION BOOT UP 🖥️
*/
app.listen(PORT, () => {
    console.log(`App running on port ${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

In the fourth part of the snippet above, we have configured our app to handle 404 error case and general errors that may occur from anywhere in the application.

A few vital things to note:

  • We have added error handling at part4 after routes for the application have been registered at part3. This way, incase of incoming request requiring resource on non-existent path, a 404 error handler will catch and process this error. And it is called only after no route has been matched in the routes registered at part3.
  • We have added General error handler middleware as the last-most middleware in the express middleware chain. It is important to declare it last so that any unhandled errors that occur before it, can be captured and handled.

Hence when it comes to effective error handling in express, order of placement of the error handlers is important.

Cheers!🥂. Up to this point, your program should be equipped to gracefully handle unexpected errors that may occur in route handlers and middlewares.

Infact, let us test run by introducing error in our application.

4. Error Handling in action

To test Error Handling in our application, let's add another test route. So once again, open index.js file which should be found at the path: server/src/.

Edit part 3 this file to add a new route, like shown:

const CustomError = require("./config/errors/CustomError.js");

// ...

/* 
  3. APPLICATION ROUTES 🛣️
*/
// ...
// Test Crash route
app.get("/boom", function (req, res, next) {
    try {
        throw new CustomError("Oops! matters are chaotic💥", 400);
    } catch (error) {
        next(error);
    }
});

// ...
Enter fullscreen mode Exit fullscreen mode

We have added a new route,(/boom) and its route handler.

Note: The route handler is written with the signature of express middleware, i.e it takes three parameters. Writing as an express middleware enables us to pass any errors generated here to the application's error handler we have configured using the next() function, which is available as the third parameter in the route handler.

This route handler throws an error inside a try...catch construct, using our predefined CustomError class. We have passed a string message and a number for the HTTP status code we would like to return in the response. This will create an error object that will be caught by catch(). Inside the catch(){...} block, we simply forward the caught error to our configured error handling middleware with a single line of code: next(error). And that is it🦸.

Run the program. And when you run it, open your browser or any other client you prefer e.g postman. Then navigate to the route that throws an error, i.e http://localhost:8000/boom.

You should see that the error thrown in our application was handled deftly by error handler configured. And of course, our application is still running. It did not shut down.

Notice that the string message and HTTP status code passed to CustomError constructor are included in the response body and header respectively:

Internally handled error response
HTTP request/response made with curl

Final thoughts

Well that is how you handle exceptions that can occur while running route handlers and middlewares in express. Possibly this is a compelling way since it reduces boiler plate code you need to write in the catch(){...} block, especially in the route handlers. Additionally, "regular code" in route handlers is separated from "error handling code".

Using Custom Error constructors to create Error objects when we throw is important for providing additional properties to the Error generated. The error handler can then obtain the properties on an error and respond to a user with concise information about what went wrong.

In the example where we test to see error-handling in action, we wrapped logic for the route handler inside try...catch construct. If all we are going to write is synchronous code only, then we do not need to explicitly call next(error) function with the error caught which is the case inside catch(){...} block. Therefore we can throw without the try...catch construct and Express will automatically pass the error to error-handling middleware. But it is worthy to avoid writing code that is magical🪄.

However if you write asynchronous operations, you must call next(error) function with the error returned, so it may be caught by the error-handling middleware. For example we may write a route handler that has asynchronous operation like this:

app.get("/boom", async function (req, res, next) {
    const iAlwaysReject = new Promise((resolve, reject) => {
        setTimeout(() => reject("Operation failed!"), 2000);
    });
    try {
        await iAlwaysReject(); // Asynchronous OP
    } catch (error) {
        next(error); // Explicitly call next
    }
});
Enter fullscreen mode Exit fullscreen mode

Note: In Express 5 and above, asynchronous operations that reject or throw an error will call next(error) function automatically with the thrown error or the rejected value. If no rejected value is provided, next will be called with a default Error object provided by the Express router. With above example, it means you can await without wrapping in try...catch construct nor calling next(error) explicitly. But why write code that is magical? 🤷‍♂️

Express ships with a default error handler. Of course it is not feature rich as you may want to create your own. I guess it is good enough to get you started.

Do you have other opinions? Drop them in the comment section🕳🤾. Meanwhile, I hope you found the article useful and...peace✌️.


Follow me @twitter. I share content that may be helpful to you😃.

Top comments (0)