简体   繁体   中英

express.js async router and error handling

I have an async function as a route handler, and i'd like to have errors handled as some kind of middleware. Here is my working attempt:

router.get(
  "/",
  asyncMiddleware(
    routeProviderMiddleware(
      async ({ y }) => ({
        body: await db.query({x: y})
      })
    )
  )
)

// This is the middleware that catches any errors from the business logic and calls next to render the error page
const asyncMiddleware = fn =>
  (req, res, next) => {
    Promise.resolve(fn(req, res, next))
      .catch(next)
  }

// This is a middleware that provides the route handler with the query and maybe some other services that I don't want the route handler to explicitly access to
const routeProviderMiddleware = routeHandlerFn => async (req, res) => {
  const {status = 200, body = {}} = await routeHandlerFn(req.query)
  res.status(status).json(body)
}

What I strive to is a way to make the route declaration cleaner - I don't want the 2 middleware wrappers there, ideally i'd like for the business logic function there only, and somehow declare that every route is wrapped in these. Even combining the two middlewares together would be nice, but I didn't manage.

I use following approach:

Create asyncWrap as helper middleware:

const asyncWrap = fn =>
  function asyncUtilWrap (req, res, next, ...args) {
    const fnReturn = fn(req, res, next, ...args)
    return Promise.resolve(fnReturn).catch(next)
  }

module.exports = asyncWrap

All your routes/middlewares/controllers should use this asyncWrap to handle errors:

router.get('/', asyncWrap(async (req, res, next) => {
  let result = await db.query({x: y})
  res.send(result)
}));

At app.js , the last middleware will receive the errors of all asyncWrap :

// 500 Internal Errors
app.use((err, req, res, next) => {
  res.status(err.status || 500)
  res.send({
    message: err.message,
    errors: err.errors,
  })
})

Express 5 automatically handles async errors correctly

https://expressjs.com/en/guide/error-handling.html currently says it clearly:

Starting with Express 5, route handlers and middleware that return a Promise will call next(value) automatically when they reject or throw an error. For example:

 app.get('/user/:id', async function (req, res, next) { var user = await getUserById(req.params.id) res.send(user) })

If getUserById throws an error or rejects, next will be called with either 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.

I have shown that in an experiment at: Passing in Async functions to Node.js Express.js router

This means that you will be able to just make the callback async and use await from it directly without any extra wrappers:

router.get("/", async (req, res) => 
  const obj = await db.query({x: req.params.id})
  // Use obj normally.
)

and errors will be correctly handled automatically.

Express permits a list of middlewares for a route and this approach sometimes works for me better than higher-order functions (they sometimes look like an overengineering).

Example:

app.get('/',
  validate,
  process,
  serveJson)

function validate(req, res, next) {
  const query = req.query;
  if (isEmpty(query)) {
    return res.status(400).end();
  }
  res.locals.y = query;
  next();
}

function process(req, res, next) {
  Promise.resolve()
  .then(async () => {
    res.locals.data = await db.query({x: res.locals.y});
    next();
  })
  .catch((err) =>
    res.status(503).end()
  );
}

function serveJson(req, res, next) {
  res.status(200).json(res.locals.data);
}

What you can do is add an error handlers after your routes. https://expressjs.com/en/guide/error-handling.html

app.use(function (err, req, res, next) {
  console.error(err.stack)
  res.status(500).send('Something broke!')
})

What I ended up doing is unifying the wrappers like this:

const routeProvider = routeHandlerFn => async (req, res, next) => {
  try {
    const {status = 200, body = {}} = await routeHandlerFn(req.query)
    res.status(status).json(body)
  } catch(error) {
    next(error)
  }
}

This wrapper is all any route would need. It catches unexpected errors and provides the route handler with the needed params.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM