How does this asyncHandler function work?

Viewed 497

I'm talking about this function:

const asyncHandler = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);

I am keen to understand step by step what is going on when we pass it this function:

getBootcamps = asyncHandler(async (req, res, next) => {
  const bootcamps = await Bootcamp.find();
  res.status(200).json({ success: true, count: bootcamps.length, data: bootcamps });
});
2 Answers

It generates a new function, based on existing function fn, with the signature:

(req, res, next)

This generated function's body is:

return Promise.resolve(fn(req, res, next)).catch(next)

What it does is:

  • call the fn (the underlying handler) with arguments (req, res, next)
  • pass the return value of that fn call to Promise.resolve() - this converts it to a Promise if it wasn't one already
  • register a callback (next) for when the promise rejects

The code that you've posted seems to be an Adapter that converts an async function to work with next(err)-based error handling (most likely for the express framework). Notably, however, it only seems to care about the error case - it is still the async function's responsibility to send the response, thus ending the request processing.

Some other Web frameworks exist that make this step unnecessary - for instance, fastify can just use async functions as handlers directly and simply return the response object instead of doing an explicit .json() call (which does res.end() under the hood).

A combinator is a higher-order function that uses only function application and earlier defined combinators to define a result from its arguments.

So we have a combinator, asyncHandler -

const asyncHandler = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next)

And we have getBootcamps -

const getBootcamps = asyncHandler(...)

So we fill in the definition of asyncHandler -

const getBootcamps = (fn) => (req, res, next) =>
  Promise.resolve(fn(req, res, next)).catch(next)

Where the first argument, fn, is equal to -

async (req, res, next) => {
  const bootcamps = await Bootcamp.find();
  res.status(200).json({ success: true, count: bootcamps.length, data: bootcamps });
}

So that becomes -

const getBootcamps = (req, res, next) =>
  Promise.resolve((async (req, res, next) => {
    const bootcamps = await Bootcamp.find();
    res.status(200).json({ success: true, count: bootcamps.length, data: bootcamps });
  })(req, res, next)).catch(next)

So as asyncHandler's name implies, it accepts a fn which is treated as an async handler. fn can return a promise and asyncHandler will properly wrap it and automatically connect the next callback to properly handle errors. Related: What do multiple arrow functions mean in JavaScript?

This is an effective combinator because it prevents you from having to write this try-catch boilerplate for every async handler you would need -

// without asyncHandler 

async function  getBootcamps (req, res, next) {
  try {
    const bootcamps = await Bootcamp.find()
    res.status(200).json({ success: true, count: bootcamps.length, data: bootcamps })
  }
  catch (err) {
    next(err)
  }
}

Instead asyncHandler allows you to focus on "success" path and automatically handles the "error" path for you -

// with asyncHandler

const getBootcamps = asyncHandler(async (req, res, next) => {
  const bootcamps = await Bootcamp.find()
  res.status(200).json({ success: true, count: bootcamps.length, data: bootcamps })
})
Related