Search code examples
javascriptexpressjestjssupertest

Jest await default error handler of express


Given the following setup:

const express = require("express");
const app = express();

app.get("/", function(req, res, next) {
  // explicitly return an error
  return next("my error");
});

// made this middleware for the example,
// solution should typically also work for express default error handling
app.use(function(error, req, res, next) {
  if (error) {
    res.status(500).send({ error });
    throw new Error(error); // <- how to test this?
  }
  next();
});

app.listen(8080, function() {
  console.log("server running on 8080");
}); //the server object listens on port 8080

And for the test:

const request = require("supertest");
const app = require("../../app.js");

const spy = jest.spyOn(global.console, "error").mockImplementation();

it("throws an error", async done => {
  const res = await request(app).get("/");

  expect(res.status).toBe(500);
  expect(res.error.text).toContain("my error");

  expect(spy).toHaveBeenCalled(); // nothing...

  done();
});

Made a Codesandbox with this example code. Not sure how to run a node test in that though.


Solution

  • async shouldn't be used with done, this results in test timeout in case done() cannot be reached.

    First of all, error handler shouldn't re-throw an error, unless it's reusable router instance that is supposed to be augmented with another handler. If it's the last one in a row, it should catch both synchronous and asynchronous errors that can happen inside of it.

    The problem is that default error handler is triggered asynchronously so it should be specifically awaited:

    it("throws an error", async () => {
      const spy = jest.spyOn(global.console, "error");
      const res = await request(app).get("/");
    
      expect(res.status).toBe(500);
      expect(res.error.text).toContain("my error");
      await new Promise(resolve = > setTimeout(resolve));
      expect(spy).not.toHaveBeenCalled(); // it really shouldn't
    });
    

    A more correct way to approach this is to make sure the error is handled:

    it("throws an error", async () => {
      const defaultErrorHandler = jest.fn((err, req, res, next) => {});
    
      app.use(defaultErrorHandler);
      const res = await request(app).get("/");
    
      expect(res.status).toBe(500);
      expect(res.error.text).toContain("my error");
      expect(defaultErrorHandler).not.toHaveBeenCalled();
    });