Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Node.js UnhandledPromiseRejectionWarning even after catching it

I'm using Node 7.2.1 with the new async/await feature. I'm also using the Native ES6 Promises with mongoose like this -

const mongoose = require('mongoose');
mongoose.Promise = global.Promise;

My code flow is like this -

async function getFollowers(){
    try {
        const followers = await User.getFollowersFromMongo(req.params.userId);
        res.send(followers);
    } catch (err) {
        winston.error('Printing Error = ', err);
        res.status(400).send({success: false, error: err});
    }
}

UserSchema.statics.getFollowersFromMongo = async(userId) => {
    try {
        let aggregateQuery = []; //some syntactical error in mongo query to produce exception

        const followers = await User.aggregate(aggregateQuery);
        return followers.map(follower => follower.followerData);
    }
    catch (err) {
        return Promise.reject(err);
    }
};

This code works absolutely fine. Problem arises when there's some error produced. So I purposely modified my mongoose query so that MongoDB would throw an error.

Now MongoDB, as expected throws an error which is perfectly caught by my code and returned to the client with a 400 error code.

The Problem is that even though the error(intentional) has been caught by me, Node.js still gives me this warning -

error:  Printing Error = MongoError: path option to $unwind stage should be prefixed with a '$': followerData
at Function.MongoError.create (/home/node_modules/mongodb-core/lib/error.js:31:11)
at /home/node_modules/mongodb-core/lib/connection/pool.js:483:72
at authenticateStragglers (/home/node_modules/mongodb-core/lib/connection/pool.js:429:16)
at Connection.messageHandler (/home/node_modules/mongodb-core/lib/connection/pool.js:463:5)
at Socket.<anonymous> (/home/node_modules/mongodb-core/lib/connection/connection.js:317:22)
at emitOne (events.js:96:13)
at Socket.emit (events.js:188:7)
at readableAddChunk (_stream_readable.js:176:18)
at Socket.Readable.push (_stream_readable.js:134:10)
at TCP.onread (net.js:551:20)

GET /user/385/followers 400 39.868 ms - 263

(node:10158) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): MongoError: path option to $unwind stage should be prefixed with a '$': followerData
(node:10158) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

As it can be seen my request has returned the 400 status and my error log has also been printed from the initial method's catch block but Node.js is still saying that the error message is not handled.

Why is it saying this even after the same error has been caught?

Update - Thanks to @dvlsg and @Bergi, the bug has been fixed in version 4.7.5

like image 853
Jyotman Singh Avatar asked Dec 20 '16 16:12

Jyotman Singh


1 Answers

There definitely appears to be something strange with how mongoose aggregate plays with async/await. Seems like a bug, to me. If it is, it should definitely be reported to mongoose.

Thankfully, there is an easy work around:

const followers = await User.aggregate(aggregateQuery).exec();

Adding the explicit .exec() allows me to catch the aggregate pipeline error as expected.


I think the underlying issue adding to the confusion here is that there is an additional Promise floating around which is being rejected and not handled. Because technically, you are handling the expected rejection here correctly. Otherwise you wouldn't see that Printing error = ... being logged.

Here's what I believe is happening --

  • You await User.aggregate()
  • Aggregate#then() is called through await working with thenables (I think)
  • Aggregate#exec() is called internally by Aggregate#then()
    • Note that a callback is provided to exec()
  • A new Promise inside Aggregate#exec() is created, and will be rejected
    • This is the unhandled Promise, I believe.
  • Since a callback is provided to Aggregate#exec() from Aggregate#then(), the Error inside Aggregate#exec() will be provided to the callback
  • Inside the callback in Aggregate#then(), a newly created Promise is rejected
    • I believe this Promise is handled as expected, since it is the return from Aggregate#then()

I think I can confirm my suspicions by commenting out this line in the mongoose Aggregate definition. This will prevent the unhandled rejection handler from being hit. Not that I'm suggesting to do that, by the way. That's just additional evidence, not a solution, since now I just have an unrejected Promise floating around.


Here is a minimal-ish way to reproduce the uncaught rejection in a self contained bit of code, to be run with node --harmony-async-await (tested on node v7.2.1)

const mongoose = require('mongoose');
mongoose.Promise = global.Promise;
mongoose.connect('mongodb://localhost/temp');

const userSchema = new mongoose.Schema({
  name: 'string'
});

const User = mongoose.model('User', userSchema);

async function run() {
  try {
    await User.aggregate([]);
  }
  catch (err) {
    console.log('caught expected error:', err);
  }
}

run();
like image 136
dvlsg Avatar answered Oct 21 '22 02:10

dvlsg