javascriptnode.jsmongoosepromiseco

promises with mongoose and es6 not working as expected


I have the following code which creates an array of promises to save some numbers, then it yields the promises (using co library) and prints out the results. What I don't understand, however, is that when it prints the output, it prints the same record 10 times.

Here is the code:

'use strict'
const Promise = require('bluebird');
const co = require('co');
const _ = require('lodash');
const mongoose = require('mongoose');

// plug in the bluebird promise library for mongoose
mongoose.Promise = Promise;

mongoose.connect('mongodb://localhost:27017/nodejs_testing');

const numSchema = new mongoose.Schema({
  num: { type: Number, required: true }
});
const Num = mongoose.model('Num', numSchema);

let promises = [];
let x;

// create an array of promises to save some numbers
for (let i = 0; i < 10; ++i) {
  let p = new Promise((resolve,reject) => {
    x = Num();
    x.num = i;
    x.save((err) => {
      if (err) {
        reject(err);
      } else {
        resolve(x);
      }
    });
  });
  promises.push(p);
};

// yield all the promises, then print out the results
co(function * () {
  let res = yield Promise.all(promises);
  _.each(res, item => {
    console.log(JSON.stringify(item));
  });
  mongoose.disconnect();
});

Here is the output:

/tmp/test$ node m
{"__v":0,"num":9,"_id":"57d1931037a370055f51977c"}
{"__v":0,"num":9,"_id":"57d1931037a370055f51977c"}
{"__v":0,"num":9,"_id":"57d1931037a370055f51977c"}
{"__v":0,"num":9,"_id":"57d1931037a370055f51977c"}
{"__v":0,"num":9,"_id":"57d1931037a370055f51977c"}
{"__v":0,"num":9,"_id":"57d1931037a370055f51977c"}
{"__v":0,"num":9,"_id":"57d1931037a370055f51977c"}
{"__v":0,"num":9,"_id":"57d1931037a370055f51977c"}
{"__v":0,"num":9,"_id":"57d1931037a370055f51977c"}
{"__v":0,"num":9,"_id":"57d1931037a370055f51977c"}

Note that if I declare the variable x inside the Promise, then I get the expected results (e.g. 10 different numbers in the output). In other words, if I make this change (see below), it works as expected:

  let p = new Promise((resolve,reject) => {
    let x = Num(); // <--- declare x inside the promise
    .
    .
  });

My question is, why does the code behave this way? Note that if I repeat the exact same type of test not using mongodb/mongoose and just printing some numbers, it works as expected even with x declared outside the Promise. Sample code below:

'use strict'
const Promise = require('bluebird');
const co = require('co');
const _ = require('lodash');

class Number {
  constructor(num) {
    this.num = num;
  }
};

let x;
let promises = [];

for (let i = 0; i < 10; ++i) {
  let p = new Promise((resolve,reject) => {
    setTimeout(() => {
      x = new Number(i);
      resolve(x);
    }, 300);
  });
  promises.push(p);
};

co(function * () {
  let res = yield Promise.all(promises);
  _.each(res, item => {
    console.log(JSON.stringify(item));
  });
});

Output:

/tmp/test$ node t
{"num":0}
{"num":1}
{"num":2}
{"num":3}
{"num":4}
{"num":5}
{"num":6}
{"num":7}
{"num":8}
{"num":9}

Solution

  • The difference isn't Mongoose vs. non-Mongoose. Your code is doing different things.

    In your first example, you have (see *** comments):

    let p = new Promise((resolve,reject) => {
      x = Num();               // *** A
      x.num = i;
      x.save((err) => {
        if (err) {
          reject(err);
        } else {
          resolve(x);          // *** B
        }
       });
    });
    

    ...where x is declared outside the loop that code is in, so all iterations reuse the variable.

    Note that the statements marked A and B above happen asynchronously to each other. By the time B happens, all of the iterations have already done A; since B sees the last value assigned to x, that's what it uses to resolve, and they're all resolved with the same value.

    Compared with your second example:

    let p = new Promise((resolve,reject) => {
      setTimeout(() => {
        x = new Number(i);     // *** A
        resolve(x);            // *** B
      }, 300);
    });
    

    Note that the two are now happening synchronously with each other; B uses the then-current value of x each time it does the resolution.

    That's the reason for the difference in behavior between the two.

    Fundamentally, x should be declared a lot closer to where it's used, within the promise init callback:

    //let x;                         // *** Not here
    
    // create an array of promises to save some numbers
    for (let i = 0; i < 10; ++i) {
      let p = new Promise((resolve,reject) => {
        let x = Num();               // *** Here
        x.num = i;
        x.save((err) => {
          if (err) {
            reject(err);
          } else {
            resolve(x);
          }
         });
      });
    }
    

    Remember the rule is: Always declare in the narrowest scope you can.