Handling errors in express async middleware
Asked Answered
D

6

82

I have an async middleware in express, because I want to use await inside it, to clean up my code.

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

app.use(async(req, res, next) => {
    await authenticate(req);
    next();
});

app.get('/route', async(req, res) => {
    const result = await request('http://example.com');
    res.end(result);
});

app.use((err, req, res, next) => {

    console.error(err);

    res
        .status(500)
        .end('error');
})

app.listen(8080);

The problem is that when it rejects, it doesn't go to my error middleware, but if I remove the async keyword and throw inside a middleware it does.

app.get('/route', (req, res, next) => {
    throw new Error('Error');
    res.end(result);
});

So I'm getting UnhandledPromiseRejectionWarning instead of entering my error handling middleware, how can I let the error bubble up, and express handle it?

Duplication answered 17/7, 2018 at 22:35 Comment(1)
Here's a good article I found when i was googling about it: strongloop.com/strongblog/…Brachiate
D
137

The problem is that when it rejects, it doesn't go to my error middleware, but if I remove the async keyword and throw inside a middleware it does.

express doesn't support promises currently, support may come in the future release of [email protected]

So when you pass a middleware function, express will call it inside a try/catch block.

Layer.prototype.handle_request = function handle(req, res, next) {
  var fn = this.handle;

  if (fn.length > 3) {
    // not a standard request handler
    return next();
  }

  try {
    fn(req, res, next);
  } catch (err) {
    next(err);
  }
};

The problem is that try/catch won't catch a Promise rejection outside of an async function and since express does not add a .catch handler to the Promise returned by your middleware, you get an UnhandledPromiseRejectionWarning.


The easy way, is to add try/catch inside your middleware, and call next(err).

app.get('/route', async(req, res, next) => {
    try {
        const result = await request('http://example.com');
        res.end(result);
    } catch(err) {
        next(err);
    }
});

But if you have a lot of async middlewares, it may be a little repetitive.

Since I like my middlewares as clean as possible, and I usually let the errors bubble up, I use a wrapper around async middlewares, that will call next(err) if the promise is rejected, reaching the express error handler and avoiding UnhandledPromiseRejectionWarning

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

module.exports = asyncHandler;

Now you can call it like this:

app.use(asyncHandler(async(req, res, next) => {
    await authenticate(req);
    next();
}));

app.get('/async', asyncHandler(async(req, res) => {
    const result = await request('http://example.com');
    res.end(result);
}));

// Any rejection will go to the error handler

There are also some packages that can be used

Duplication answered 17/7, 2018 at 22:35 Comment(6)
Also worth mentioning that the the framework Koa (supposedly built by people who first worked on Express) has more built-in conveniences for async operations in request handlers and the kind of Promise awareness and error handling the OP was asking about.Thun
Typescripted: export const asyncHandler = (fn: RequestHandler) => (req: Request, res: Response, next: NextFunction) => Promise.resolve(fn(req, res, next)).catch(next). Thanks!Winograd
More info on the [email protected] support: expressjs.com/en/guide/error-handling.html. I've tried 5.0.0-alpha.8, and it works exactly as described: route handlers and middleware that return a Promise will call next(value) automatically when they reject or throw an error..Fermi
Why can't asyncHandler be an async function itself too?Rugby
I'm a bit confused, Can you explain why you used Promise.resolve(fn(...)).catch() instead of simply fn(...).catch() ?Columniation
@Columniation otherwise this wrapper function is not asynchronous. And the async again is needed because we also use an async function as fn, which in it's turn is calling await request(...). Anyhow, I really really need Express 5.x!Frum
S
49

Well, I found this - https://github.com/davidbanham/express-async-errors/, then require the script and you are good to go

const express = require('express');
require('express-async-errors');
Slat answered 4/12, 2019 at 12:42 Comment(5)
This module is still useful. As mentioned below, Express v5 will add support for async middlewares, but as of today there is no release of v5.Insipience
Too bad it doesn't work with routersTricycle
You saved my day. Thks :)Goldsmith
If you use a Router, you just have to require it at the beginning of each router fileGuttery
Latest version of this library is from 2018 :\? With quite some issues in GitHub.Frum
P
19

Answer with asyncHandler is good and usefull, but it is still not comfortable to write this wrapper in every route. I propose to improve it:

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

const methods = [
    'get',
    'post',
    'delete'  // & etc.
]

function toAsyncRouter(router) {
    for (let key in router) {
        if (methods.includes(key)) {
            let method = router[key]
            router[key] = (path, ...callbacks) => method.call(router, path, ...callbacks.map(cb => asyncHandler(cb)))
        }
    }
    return router
}

and now we can do that way:

const router = toAsyncRouter(express().Router())
router.get('/', someAsyncController)

and so one.

Minute ago added a npm module async-express-decorator.

Paramagnetic answered 18/7, 2019 at 16:42 Comment(0)
C
8

Express 5 now handle async promises:

https://expressjs.com/en/guide/error-handling.html

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

Crinoid answered 14/6, 2021 at 15:29 Comment(3)
Doesn't seem to work for meElectromotive
when will express 5 be released?Argentum
5.0.0-beta.1 is out now. I was using 4.18.2 and everything works the same, and now async error handling works.Heikeheil
K
7

You need to use try-catch and in catch section just pass the error in next() parameter Like this -

async create(req, res, next) {

    try {
      const userProp = req.body;
      const user = new User(userProp)

      const response = await user.save()
      const token = await user.createJWSToken()

      res.send({response, token})

    } catch (err){
      next(err)
    }
}

And obviously put this express middleware on your index.js file.

app.use((err, req, res, next) => {
  res.status(422).send({ error: err.message });
});
Kaslik answered 28/11, 2019 at 14:22 Comment(1)
But this sets a common 422 error code for all responses. What's wrong if I don't set up an error middleware and deal with all the errors inside the handler code, returning the wanted JSON and status?Pistole
R
4

You need to callbackify your async handler. If you know the concept of promisify, this is the opposite. Callbackify is built-in in Node.

import util from 'util'

app.use(util.callbackify(async (req, res) => {
  await authenticate(req);
}));

What this does is that it returns a function with a third argument which would be the next function and calls it after the promise has been resolved. If the promise is rejected, the next function will be called with the error as an argument.

Runty answered 20/11, 2020 at 11:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.