Reputation: 730
This router is used to get access to a specific list. In the example comments
is an array of objects. When i insert this gravatar.url(data.Email, {s: '200', r: 'pg', d: '404'});
in comments
object its successfully done. But on the front end (i use jade). I try to use this list.comments[i].gvUrl
, it returns undefined. It even returns undefined just out side the for loop! What am i doing wrong?
router.get('/:id', function (req, res, next) {
List.findOne({listurl: req.params.id}, function (err, doc) {
var z = 0;
if (!err && doc != null) {
for (var i = 0; i < doc.comments.length; i++) {
User.findOne({Name: doc.comments[i].commenter}, function (err, data) {
if (data) {
doc.comments[z++].gvUrl = gravatar.url(data.Email, {s: '200', r: 'pg', d: '404'});
} else {
doc.comments[z++].gvUrl = 'noGravs';
}
});
}
//this line returns unfefined;
console.log(doc.comments[0].gvUrl);
res.render('list', {
appTitle: doc.Title,
list: doc
});
}
else {
res.status(404).render('404', {appTitle: "Book not found"});
}
});
});
Upvotes: 0
Views: 65
Reputation: 708116
You are initiating a bunch of asynchronous operations inside a for
loop and expecting all the asynchronous operations to be done when the for
loop is done. But, in reality, none of them are yet done so your doc.comments
array is not yet populated. You were trying to use it before it had been populate so that's why you see it as defined where you were trying to use it.
The best way to solve this is to learn how to use Promises and then fire multiple requests with something like Blubird's Promise.map()
or ES6 Promise.all()
and then let the promise engine tell you when all the requests are done.
Short of converting your database calls over to use Promises, you can hand-code to know when everything is done as follows:
Hand-coded Callback Implementation
router.get('/:id', function (req, res, next) {
List.findOne({listurl: req.params.id}, function (err, doc) {
var doneCnt = 0;
if (!err && doc != null) {
for (var i = 0; i < doc.comments.length; i++) {
(function(index) {
User.findOne({Name: doc.comments[i].commenter}, function (err, data) {
++doneCnt;
if (err) {
// need some form of error handling here
doc.comments[index].gvUrl = "";
} else {
if (data) {
doc.comments[index].gvUrl = gravatar.url(data.Email, {s: '200', r: 'pg', d: '404'});
} else {
doc.comments[index].gvUrl = 'noGravs';
}
}
// if all requests are done now
if (doneCnt === doc.documents.length) {
res.render('list', {
appTitle: doc.Title,
list: doc
});
}
});
})(i);
}
}
else {
res.status(404).render('404', {appTitle: "Book not found"});
}
});
}
This code recognizes the following things about your async operations:
User.findOne()
async operations in a loop.Bluebird Promise Implementation
Here's how it could work using the Bluebird Promises library and converting your database operations to support Promises:
var Promise = require('bluebird');
// promisify the methods of the List and User objects
var List = Promise.promisifyAll(List);
var User = Promise.promisifyAll(User);
router.get('/:id', function (req, res, next) {
List.findOneAsync({listurl: req.params.id}).then(function(doc) {
if (!doc) {
throw "Empty Document";
}
return Promise.map(doc.comments, function(item, index, length) {
return User.findOneAsync({Name: item.commenter}).then(function(data) {
if (data) {
item.gvUrl = gravatar.url(data.Email, {s: '200', r: 'pg', d: '404'});
} else {
item.gvUrl = 'noGravs';
}
});
}).then(function() {
return doc;
});
}).then(function(doc) {
res.render('list', {
appTitle: doc.Title,
list: doc
});
}).catch(function(err) {
res.status(404).render('404', {appTitle: "Book not found"});
});
}
Here's how this works:
User
and List
objects so it adds a new version of each method that returns a promise.List.findOneAsync()
. The "Async"
suffix on the method name represents the new method that the .promisifyAll()
added.doc
, then throw which will reject the promise will then be handled in the .catch()
at the end. Promises are async throw-safe which is very handy for async error handling.Promise.map()
on the doc.comments
. This will automatically iterate the doc.comments
array and call an iterator on each item in the array (similar to Array.prototype.map()
except here it collects all the promises returned by the iterator and returns a new promise that is resolved when all the underlying promises are resolved. In this way, it allows all the iterators to run in parallel and tells you when all the iterators are done.User.findOneAsync()
and sets the doc.comments[index].gvUrl
value with the result..then()
handler on the Promise.map()
just to change the resolved value of that promise to be the doc
object so we can get at that from the outer promise handlers.ES6 Promise Implementation
This could be done with straight ES6 promises without the Bluebird promise library, but you'd have to do a few more things by hand:
List.findOne()
operation.User.findOne()
operation.doc.comments.map()
iteration and collect each individual promise into an array and then use Promise.all()
on that array instead of letting Promise.map()
do all of that for you.Here's the code:
// manually promisify findOne
List.findOneAsync = function(queryObj) {
return new Promise(function(resolve, reject) {
List.findOne(queryObj, function(err, data) {
if (err) return reject(err);
resolve(data);
});
}
}
User.findOneAsync = function(queryObj) {
return new Promise(function(resolve, reject) {
User.findOne(queryObj, function(err, data) {
if (err) return reject(err);
resolve(data);
});
}
}
router.get('/:id', function (req, res, next) {
List.findOneAsync({listurl: req.params.id}).then(function(doc) {
if (!doc) {
throw "Empty Document";
}
var promises = doc.comments.map(function(item, index) {
return User.findOneAsync({Name: item.commenter}).then(function(data) {
if (data) {
item.gvUrl = gravatar.url(data.Email, {s: '200', r: 'pg', d: '404'});
} else {
item.gvUrl = 'noGravs';
}
});
});
return Promise.all(promises).then(function() {
return doc;
});
}).then(function(doc) {
res.render('list', {
appTitle: doc.Title,
list: doc
});
}).catch(function(err) {
res.status(404).render('404', {appTitle: "Book not found"});
});
}
Upvotes: 1