The grass is rarely greener, but it's always different

Async me gusta, async exception handling in express.js

How do you handle exceptions in express.js? If you're anything like me, you start coding your application with your best intentions and favor readability, simplicity, and separation of concerns.

At the beginning I tend to split the app with a MVC structure featuring routes, middleware, controllers and I make use of services to group related pieces of functionality, trying to follow best practices, classic.

Once the application begins to grow and edge cases start biting my ass, I start to sprinkle try/catch blocks here and there, sometimes at the controller level, sometimes for some of the service methods, and sometimes within utility functions, following several layers of function calls.

Then I arrive to a common crossroads: do I deal with the exception right in the function (oftentimes returning a custom json error for the frontend to deal with it) or I throw an exception and bubble it up?

When I started developing the chat application at Websie, initially this is the quick and dirty way I had to deal with an error:

const myController = async (req, res, next) => {
    await myService.myServiceDbAccess(req, res);
    return res.status(200).json({ 'status': 'ok' });
}

const myServiceDbAcces = async (req, res) => {
    try {
        Model.create({ badly shaped object })
    }
    catch (e) {
        return generateServerErrorCode(res, 500, 'Wrong object shape');
    }
}


const generateServerErrorCode = (res, code, fullError, msg) => {
    let errors = {};
    errors = {
      fullError,
      msg,
    };
    return res.status(code).json({
        code,
        errors,
    });
};

I would catch the exception in the service directly and return a custom error by passing the response object to the generateServerErrorCode function with additional data.

Knowing this was bad practice upfront, it quickly became annoying and hard to maintain. I would need to pass the Response object down the functions and I didn't know where to look for initially when an exception happened.

I set out to write a simple® and extensible way to deal with exceptions. I found two useful articles I loosely combined to achieve it. First, I wanted to deal with the Request & Response objects and exceptions in a single place, and use an object hierarchy for exceptions to help me identify and manage them in a structured manner.

If an error were to happen anywhere in the application, just throw the corresponding exception and let the uppermost handler settle it.

I wrote this function wrapper:

const errorStatusCodes = {
    [NOT_ALLOWED]: 403,
    [NOT_SUPPORTED]: 500,
    ...etc
}


/**
 * Check if the Error object inherits from an Error we know.
 * @param {object} error 
 * @returns 
 */
const isKnownError = error => {
    if (
        error instanceof AppointmentConfirmError ||
        error instanceof AppointmentCreateError ||
        error instanceof AppointmentDateConflictError ||
        error instanceof AppointmentInThePastError ||
        error instanceof WebsieChatError
    ) {
        return true;
    }
    else {
        return false;
    }
}


const asyncTryCatch = (fn, errorCallback = async () => {}) => {
    return async (req, res, next) => {
        try {
            await fn(req, res, next);
        } catch (error) {
            if (isKnownError(error)) {
                const { status, message } = error;
                const errorStatus = errorStatusCodes[status];

                //Call the errorCallback with the error object
                await errorCallback(error, req, res);

                return res.status(errorStatus).json({ status, message });
            }
            else {
                throw error;
            }
        }
    }
}

So in this case, whenever I need just the default handling behaviour of crafting a custom error message and send it along with it's corresponding http status, I wrap the controller with the asyncTryCatch function.

router.post(
    "/confirm-appointment",
    [
        ... bunch of middlewares ...
    ],
    asyncTryCatch(confirmAppointment)
);

And if along the chain of calls inside the confirmAppointment controller it detects, say, that a patient doesn't have a positive balance, an AppointmentConfirmError error type with an explanatory message is thrown:

throw new AppointmentConfirmError(
    'confirmAppointment',
    "Vous ne pouvez pas confirmer un rendez-vous sans avoir un solde positif",
    NOT_ALLOWED
);

This error will be caught in the wrapper function and return the error json error:

#http status = 403
{
    "status": "403",
    "message": "Vous ne pouvez pas confirmer un rendez-vous sans avoir un solde positif"
}

Using a custom error callback

In case I need to add some additional logic to handle exceptions in a given controller, I indicate an errorCallback to be called within the asyncTryCatch function like say for example sending a custom email when posting some data fails:

router.post(
    '/post-stats',
    [ ... middlewares ... ],
    asyncTryCatch(successfulPost, successfulPostExceptionHandler)
);

const successfulPostExceptionHandler = async (error, req, res) => {
    const { patientId, psyId } = req.body;
    const message = `patientId: ${patientId}, psyId: ${psyId}, message: ${JSON.stringify(error.message)}`;
    await sendMailNotificationSupport(
        POST_SUCCESS_PAYMENT_FAILED,
        'successfulPost',
        message
    );
}

Since there might be multiple kinds of exceptions thrown inside a controller and one might want to work with them differently, we could create perhaps a hierarchical error handling logic too that would do different things for different exception types, perhaps for another post.

I'm not sure if this is the way to go™ but it works for me in the meantime and has improved the way I handle errors in the application.

Have fun!

#javascript #learning #web