Shafayat Alam
Shafayat Alam

Reputation: 730

NodeJS object returns un defined

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

Answers (1)

jfriend00
jfriend00

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:

  1. You are firing multiple User.findOne() async operations in a loop.
  2. These async operations can finish in any order.
  3. These async operations will not be complete when the loop is done. All the loop has done is initiate the operations. They will all complete some indeterminate time later.
  4. To know when all of the async operations are done, it keeps a counter to count how many have completed and renders the page when the count reaches the number of total requests that were started. That's the "manual" way to how you know when all of them are done.

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:

  1. Load the Bluebird promise library
  2. Add promisified methods to the User and List objects so it adds a new version of each method that returns a promise.
  3. Call List.findOneAsync(). The "Async" suffix on the method name represents the new method that the .promisifyAll() added.
  4. If no 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.
  5. Call 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.
  6. The iterator calls User.findOneAsync() and sets the doc.comments[index].gvUrl value with the result.
  7. There's an extra .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.
  8. For success from the outer promise, render.
  9. For error from the outer promise, show the 404 page. Keep in mind that any rejected promises anywhere in this whole scheme will propagate up and be a rejection at the top level. This auto-propagation of async errors in promises is hugely useful.

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:

  1. You'd have to promisify the List.findOne() operation.
  2. You'd have to promisify the User.findOne() operation.
  3. You'd have to user a regular 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

Related Questions