1

I'm trying to call an asynchronous function (the 'child') from within another asynchronous function (the 'parent'). To make things complicated, the parent function exposes a dual API (callback + promise). This is done using the Q library's defer() and nodeify(), like so:

var parent = function(p_arg, callback) {

  var deferred = Q.defer();

  child(arg, function(err, cb_arg) {
    if(err)
      deferred.reject(err);
    else
      deferred.resolve(cb_arg);
  });

  //If a callback was supplied, register it using nodeify(). Otherwise, return a promise.
  if(callback)
    deferred.promise.nodeify(callback);
  else
    return deferred.promise;
}

Note that the child function doesn't return a promise. Now depending on the value of the argument passed to the child's callback (cb_arg), I might decide to make a fresh call to parent, albeit with a modified argument (p_arg). How do I do this keeping in mind the dual nature of the parent's API?

This is what I've been able to come up with so far:

child(arg, function(err, cb_arg) {
  if(err)
    deferred.reject(err);
  else if(cb_arg.condition) {

    /*Make a fresh call to parent, passing in a modified p_arg*/
    if(callback)
      parent(p_arg + 1, callback);
    else
      deferred.promise = parent(p_arg + 1);
      /*
        ^ Is this the correct way to do this?
        My goal is to return the promise returned by the fresh parent() call,
        so how about I just assign it to the current parent() call's deferred.promise?
      */
  } 
  else
    deferred.resolve(cb_arg);
});

UPDATE: Okay, I just had a moment of clarity regarding this problem. I now think that what actually needs to be done is the following:

child(arg, function(err, cb_arg) {
  if(err)
    deferred.reject(err);
  else if(cb_arg.condition) {

    /*Make a fresh call to parent, passing in a modified p_arg*/
    if(callback)
      parent(p_arg + 1, callback);
    else
      parent(p_arg + 1)
      .then(function(p_ret) {
          deferred.resolve(p_ret);
      }, function(p_err) {
          deferred.reject(p_err);
      });
  } 
  else
    deferred.resolve(cb_arg);
});

This pattern seems to work for my particular use case. But do let me know in case there are any glaring async-related errors with this approach.

Chitharanjan Das
  • 1,283
  • 10
  • 15

1 Answers1

0

How do I do this keeping in mind the dual nature of the parent's API?

Don't. Just always use promises when you have them available.

Btw, you can use Q.ncall instead of manually using a deferred. And you don't need to test explicitly for callbacks, nodeify will handle it if you pass undefined.

deferred.promise = parent(p_arg + 1);

Is this the correct way to do this? My goal is to return the promise returned by the fresh parent() call, so how about I just assign it to the current parent() call's deferred.promise?

You would use .then() and return a new promise from the callback to chain the actions, getting a new promise for the result of the last action.

var promise = child().then(function(res) {
    if (condition(res))
        return parent(); // a promise
    else
        return res; // just pass through the value
});

Update: […] This pattern seems to work for my particular use case.

That's a variation of the deferred antipattern. You don't want to use it. In particular, it seems to leave a dangling (never-resolved) deferred around with a callback attached to it, in the case that there is a callback passed.

What you should do instead is just

function parent(p_arg, callback) {
    return Q.nfcall(child, arg)
    .then(function(cb_arg) {
        if (cb_arg.condition) {
            return parent(p_arg + 1, callback);
        else
            return cb_arg;
    })
    .nodeify(callback);
}
Community
  • 1
  • 1
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • I should've mentioned that the child doesn't return a promise. For my particular use-case, the 'child' is node's standard http.get(). – Chitharanjan Das Nov 24 '14 at 18:11
  • I would love to always use promises. However, I came across this problem while writing my first node module, for which I thought it'd be cool to have the same async function offer to do a callback *or* return a promise, depending on how the developer wants to use it. – Chitharanjan Das Nov 24 '14 at 18:16
  • I meant you should use promises for the recursive call - when you are the consumer of your own api. That `child` doesn't return a promise is not a huge problem, as your `deferred.promise` *is* a promise for the result of the `child()` call already. – Bergi Nov 24 '14 at 18:21
  • Thanks a lot for pointing me to these links. I'm glad I have access to these anti-patterns at an early point in my experience with promises. I'm also marking your answer as correct, however I have one last question. In that `then()` handler, how do I trigger progress events to be handled by the next chained call to `then()`? – Chitharanjan Das Nov 25 '14 at 02:58
  • Hm, progress events are deprecated, inter alia just because the use case you describe is complicated to achieve. Maybe you should go back to the deferred for `child()`, and trigger a "started" event immediately (but asynchronously) on it. – Bergi Nov 25 '14 at 08:28