1

Before (trying) to switch to promises, I could have variables that were in the whole scope of the nested functions. For example, I could get a user from a database, not use it right away, then use it in a function nested far along in the chain.

So, with promises, must I explicitly pass the needed variables into the next function? Or is there some other way to get around this?

Currently my code looks something like this

exports.add = function ( req, res ) {
    var defer = q.defer()

    // check sign in and get user information as 'user' 

    function getUser ( req, res ) {
        User.findOne( { token: req.token }, function ( err, user ) {
            if ( err || !user )
                return res.json( {
                    status: "error",
                    message: "Error authenticating: " + err
                })

            return user
        })
    }

... // more functions here

defer.promise
.then( getUser )
.then( youtubeAuthenticate )
.then( getVideoId )
.then( youtubeFind )
.then( makeVideoObject )

defer.resolve( req, res )
}

The trouble is I can't access user in functions like makeVideoObject.


EDIT: Another thing that is really stumping me is how some of my functions can return a value, say videoId and that seems to be recognized by my final function, makeVideoObject, but others aren't, with no real difference between them. remain in scope, others become undefined.


EDIT2: The reason my variable videoId was "global" was because I guess I was declaring it like videoId = .... So it wasn't the return videoId that made it global, but the way of declaring it. In that vain, I did this,

function getUserData () {
    user = getUser()
    videoId = getVideoId()
    youtubeAuthenticate()
}

function getVideoData () {
    vData = youtubeFind()
}   

defer.promise
.then( getUserData )
.then( getVideoData )
.then( function (user, videoId, vData ) {
    makeVideoObject( user, videoId, vData )
})

defer.resolve()

But no joy. Everything is still undefined.


EDIT3: I've tried the responses below and any permutations I can think of. Here is what I have at the moment.

var user = getUser()
var videoId = getVideoId()
var video = q(videoId).then( youtubeFind )

q.all( [ user, video ] ).then( function( results ) {
    if ( !results[0] )
        return res.json( "Not authenticated." )

    makeVideoObject( results[0], results[1] )
})

Using the debugger, both results are undefined. Using console.log within the getUser() and youtubeFind() functions, I see they work. Here is the getUser() function, for example.

function getUser () {
    User.findOne( { token: req.token }, function ( err, user ) {
        return user
    })
}

Also note I've simplified things by adding the authenticateYoutube() stuff right into youtubeFind().


Answer: I will probably choose an answer below out of courtesy, but this was a series of misunderstandings about Node and JS that made this problem. This doesn't answer the question as posed, but here's what I was doing wrong.

First, I thought that return, when put in a nested function (closure) should be the return <VALUE> for the "root" function it was nested in. Actually, it will only be the return value for the immediate function it's in. Probably JS 101 here. It also doesn't seem to be possible to affect variables declared in parent functions from within nested functions.

Second, you need to actually construct a function in a certain way for it to work like a promise. That format is like this:

function getUser () {
    var defer = Q.defer()   // create the promise object
    User.findOne( { token: req.token }, function ( err, user ) {
        defer.resolve( user )   // add the return value to the object
    })

    return defer.promise    // return the object, with promise and value
}
Noah
  • 4,601
  • 9
  • 39
  • 52
  • 1
    You can nest promises just like you did nest callbacks if you need to access higher-scoped variables. – Bergi Jan 29 '15 at 21:34
  • Bluebird also has [Promise.bind](https://github.com/petkaantonov/bluebird/blob/master/API.md#binddynamic-thisarg---promise) – aarosil Jan 29 '15 at 21:41
  • @aarosil he is using $q so he'd have to use `Function#bind` instead. – Benjamin Gruenbaum Jan 29 '15 at 21:42
  • 1
    What do all the interim functions return (for example `youtubeAuthenticate`). Also You're sort of missing the point of promises. To start things off - Mongoose already returns promises so your q deferred isn't needed. The really nice thing here is instead of your `.add` function being in charge of the application request life cycle error handling is done in a global place and it's perfectly clear what the request flow is. – Benjamin Gruenbaum Jan 29 '15 at 21:47
  • @BenjaminGruenbaum `youtubeAuthenticate` doesn't return anything, it just authenticates with the Youtube API v3. Optionally it can return some auth object, but I don't use it. `getUser` gets the `user` object, which is needed when `makeVideoObject` is made and saved to `mongodb`. `getVideoId` looks at the `req.query.url` of the video and returns just the `videoID` from the string. Then `youtubeFind` gets info from the Youtube API. They need to be done in order, so is that not what promises are for, async? – Noah Jan 29 '15 at 21:50
  • @Noah in that case it should instead return an authenticated user (which you say is just the same user) which would incidentally also solve your scoping issues. – Benjamin Gruenbaum Jan 29 '15 at 21:51
  • @BenjaminGruenbaum (see edited comment above) No, `getUser` returns a user from `mongodb`. `youtubeAuthenticate` just authenticates my server key so I can search Youtube. I'm not authenticating that user or anything on Youtube. Just a simple API access key. – Noah Jan 29 '15 at 21:55
  • 1
    I don't understand your question very well at all then. – Benjamin Gruenbaum Jan 29 '15 at 22:05
  • @BenjaminGruenbaum I want to perform several tasks in order. Then get the values returned by all of them in at least the `makeVideoObject` function. – Noah Jan 29 '15 at 22:09
  • 1
    Do you have a working version of the code before trying to use promises? If so it would help enormously if it was made known. – Roamer-1888 Jan 30 '15 at 03:46
  • @Roamer-1888 Yes, I have a working version. – Noah Feb 01 '15 at 04:16

2 Answers2

2

If you want to run several tasks that don't depend on each other and then run a task which needs those values, you can do something like this:

Q.all([getUserData(), getVideoData()]).spread(function(user, video) {
   // user and video are available
   makeVideoObject(user, video)
});

all takes a list of promises which can be fetched in parallel; spread splits those return values into individual params which can then be used inside the callback. Is that what you're trying to do?

Evan Davis
  • 35,493
  • 6
  • 50
  • 57
  • Thank you so much. But actually there is a second function that needs to run with input from another. `getVideoData` also needs `videoId` from the `getVideoId` function. – Noah Jan 29 '15 at 23:08
  • This did not work, and `user` and `video` are both `undefined`. I've update my question with some similar code, but I also did this verbatim. – Noah Jan 31 '15 at 00:53
  • Well if those functions don't return promises, it won't work. – Evan Davis Jan 31 '15 at 18:44
  • Does the function in my question return a promise? – Noah Feb 02 '15 at 07:41
  • Looks like `getUser` doesn't return _anything_. – Evan Davis Feb 02 '15 at 16:39
2

As Benjamin Gruenbaum has said You're Missing the Point of Promises.

Promises are not simply a utility for stringing together callbacks. They are a placeholder for a future value.

So if you want to hold onto a result from somewhere midway through your list of steps, you just need to grab it:

var user = getUser();

user.then(doSomethingWithUser);
user.then(doSomethingElseWithUser);

You haven't shown us what your functions actually do, so I'll make my best guess to show you how you can gather the pieces you need and then recombine them. I'm not sure if by "q", you are referring to Q, or to $q, or to something else entirely, so I'll assume a minimum of convenience methods are available:

// All variables declared with "var" below are promises

var user = getUser();
var videoId = user.then(getVideoId); // assumes getVideoId depends on user
var youTubeAuth = user.then(youTubeAuthenticate);

var videoData = q.all([youTubeAuth, videoId]).then(function (values) {
    return getVideoData(values[0], values[1]);
});

var videoObject = q.all([user, videoId, videoData]).then(function (values) {
    return makeVideoObject(values[0], values[1], values[2]);
});
JLRishe
  • 99,490
  • 19
  • 131
  • 169
  • It doesn't look like I can use `then` on `user`, as it's a string. – Noah Jan 30 '15 at 00:03
  • 1
    @Noah it's a promise-for-string, make sure you return a promise from `getUser` function. – Esailija Jan 30 '15 at 14:42
  • @Noah If it's a string, then just wrap it in a promise. You haven't told us what promise library you're using, but in the Q library, you can simply use `Q(user)`, and in `$q`, it's `$q.when(user)`. – JLRishe Jan 30 '15 at 15:26
  • I think I must be missing something fundamental. I still get `undefined` for results. Can you see my latest edit to the post? – Noah Jan 31 '15 at 00:54
  • 1
    @Noah If you're getting `undefined`, that almost surely means that you're not returning anything from your functions. For example, you're not returning anything from the `getUserData` and `getVideoData` functions you showed us. I can only presume that the rest of your functions are similar. Not only does this mean that they won't pass a value forward, it _also_ means that you won't be able to await them. The promise chain will just go on to the next item in the chain. – JLRishe Jan 31 '15 at 14:24
  • I'm doing `console.log( user ); return user;` in the `getUser()` function and the log shows me it's a user object as expected. If I do like `q.fcall( getUser ).then( console.log )`, it says undefined. – Noah Feb 01 '15 at 00:32
  • @Noah You have a `return` statement _inside_ the _callback_ in your `getUser()` function, but the `getUser()` function does not return anything. You have to return something: `function getUser () { return User.findOne( { token: req.token }, function ( err, user ) { return user; }); }` – JLRishe Feb 01 '15 at 16:45
  • @Noah Also, I don't think the callback function there is serving any purpose. I think you can just remove it: `function getUser () { return User.findOne({ token: req.token }); }` – JLRishe Feb 01 '15 at 18:42