Michal Zalecki
Michal Zalecki
software development, testing, JavaScript,
TypeScript, Node.js, React, and other stuff

An elegant solution for handling errors in Express

Express is a microframework that according to 2018 Node.js User Survey Report is used by 4 in 5 back-end and full-stack node.js developers. Thanks to its simplicity, the always-growing range of available middleware, and active community the express userbase is still growing.

Arguably, the simplicity of Express is its most significant advantage but comes with a cost of bare-bones API for handling requests and leaves the rest to the developer. In general, it's fantastic! We're developers, and we love to roll out the solutions that we tailor to meet our requirements.

The two everyday tasks for each RESTful application is handling errors and payload validation. I like to keep my controllers lean, so for me, middleware is the way to deal with framework-ish features like these two.

Code is available on GitHub: MichalZalecki/express-errors-handling.

Payload Validation

Hapi.js is another framework for building web services in Node.js. Hapi.js extracts input validation out of controllers into the intermediate layer between router and route handlers. Hapi is very modular and its schema validation component is a separate library called Joi. We are going to use Joi with our Express application through the Celebrate, an Express middleware for Joi.

import express from "express";
import bodyParser from "body-parser";
import { Joi, celebrate, errors } from "celebrate";

const PORT = process.env.PORT || 3000;

const app = express();

app.use(bodyParser.json());

type CreateSessionBody = { email: string, password: string };

const createSessionBodySchema = Joi.object({
  email: Joi.string().email().required(),
  password: Joi.string().required(),
}).required();

app.post("/session", celebrate({ body: createSessionBodySchema }), (req, res) => {
  const { email, password } = req.body;
  const { token } = login(email, password);
  res.status(201).send({ token });
});

app.use(errors());

app.listen(PORT, () => {
  console.log(`Server listens on port: ${PORT}`);
});

Using Celebrate with Joi is a quick and easy way to validate your schema and respond with a well-formatted error message. It also helps to keep your controllers lean, so you can assume that data from the request payload is well formatted.

That's how the sample error response may look like:

{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "child \"email\" fails because [\"email\" is required]",
  "validation": {
    "source": "body",
    "keys": [
      "email"
    ]
  }
}

Runtime Errors

Some errors are accidental like database constraint violation, lost connection, third-party service timeout, and some are expected under precisely defined conditions like token expiration, incorrect input data beyond their format (trying to register with the same email twice), etc.

I aim to unify errors handling and make it possible to early return errors to avoid deeply nested code and minimize the effort to handle exceptions. I like to wrap my route handlers with enhanceHandler which is a higher order function that formats the output and sets the correct status based on the returned value from the actual route handler.

If you use TypeScript then start with defining the type for the route handler which allows specifying the type for params and body of the request.

// types.d.ts
import express from "express";
import Boom from "boom";

interface Request<TParams, TBody> extends express.Request {
  params: TParams;
  body: TBody;
}

declare global {
  type RouteHandler<TParams, TBody> = (req: Request<TParams, TBody>, res: express.Response) =>
    | void | express.Response | Boom<any>
    | Promise<void | express.Response | Boom<any>>;
}

Depending on the middleware you use it's possible to extend Express request and add additional properties.

The essential part of the enhanceHandler is utilizing Boom library. Boom is a set of factory functions for errors that correspond to HTTP errors. Boom, like Joi, is library made to use with Hapi.js but doesn't depend on it.

// enhanceHandler.ts
import express from "express";
import Boom from "boom";
import isNil from "lodash/isNil"

function formatBoomPayload(error: Boom<any>) {
  return {
    ...error.output.payload,
    ...(isNil(error.data) ? {} : { data: error.data }),
  }
}

export function enhanceHandler<T, P>(handler: RouteHandler<T, P>) {
  return async (req: express.Request, res: express.Response, next: express.NextFunction) => {
    try {
      const result = await handler(req, res);
      if (result instanceof Error && Boom.isBoom(result)) {
        res.status(result.output.statusCode).send(formatBoomPayload(result));
      }
    } catch (error) {
      // now log errors to your errors reporting software
      if (process.env.NODE_ENV !== "production" && (error.stack || error.message)) {
        res.status(500).send(error.stack || error.message);
      } else {
        res.status(500).send(Boom.internal().output.payload);
      }
    }
    next();
  };
}

To better understand each case that is handled by enhanceHandler, read the tests on GitHub.

To use enhanceHandler just pass an actual route handler as a parameter and you can now return Boom errors from your controllers.

type CreateSessionBody = { email: string, password: string };

const createSession: RouteHandler<{}, CreateSessionBody> = (req, res) => {
  const { email, password } = req.body;
  if (password !== "password") {
    return Boom.unauthorized("Invalid password");
  }
  res.status(201).send({ email, password });
};

app.post("/session", enhanceHandler(createSession));

Wrap Up

Whether you like this approach or not and consider it elegant as I do is the matter of preference. The most important is flexibility, so you adjust it to your liking. Hope you at least enjoy this poem:

Express is awesome, but Hapi is too.
Do not add more code out of the blue.
Combine the best tools and code just the glue.
You can buy me a coffee, I like a cold brew.

Photo by David Kovalenko on Unsplash.