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}
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.