Marcos Casagrande
Marcos Casagrande

Reputation: 40444

Handling errors in express async middleware

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?

Upvotes: 88

Views: 66931

Answers (6)

Marcos Casagrande
Marcos Casagrande

Reputation: 40444

Update:

express@5 was just released with support for async routes/middlewares.


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

Upvotes: 142

Sebastien Horin
Sebastien Horin

Reputation: 11069

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

Upvotes: 8

Sakari Tuominen
Sakari Tuominen

Reputation: 41

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.

Upvotes: 4

ama
ama

Reputation: 1595

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');

Upvotes: 50

Sunny Sultan
Sunny Sultan

Reputation: 1180

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 });
});

Upvotes: 7

Mikhail Dezhurko
Mikhail Dezhurko

Reputation: 201

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.

Upvotes: 20

Related Questions