1

A couple years ago I was experimenting with NodeJS and found that the "Step" library cleaned up some of my code rather nicely. When desiring to bring that code up to date, I noticed a few "red flags" on Step. (Not being updated for a couple of years, just 32 commits, etc.)

So I looked around and found Async.js, which has more features and active maintenance.

Looks nice and general. But I started trying to apply a transformation to use it instead, and might not be taking the right angle.

If I'm reading correctly, the core function of Step seems to be what Async.JS would call the "waterfall" pattern. So in Step you would write:

Step(
    function firstStepNoArgs() {
        foo.asyncCall(this);
    },
    function secondStep(err, argFromFoo) {
        if (err) {
            handleError(err);
        }

        bar.asyncCall(argFromFoo, 1, this.parallel());
        baz.asyncCall(argFromFoo, 2, this.parallel());
    },
    function thirdStep(err, argFromBar, argFromBaz) {
        if (err) {
            handleError(err);
        }

        /* etc... */
    }
);

If I didn't know any better, I might guess you'd do that in async.js like this (untested, consider it pseudocode; I'm talking about a theoretical change I haven't actually pursued yet)

function thirdStep(argFromBar, argFromBaz) {
    /* etc... */
}

async.waterfall([
    function firstStepNoArgs(callback) {
        foo.asyncCall(callback);
    },
    function secondStep(argFromFoo, callback) {
        async.parallel([
            barResult: function(callback) {
                bar.asyncCall(parameterFromFoo, 1, callback);
            },
            bazResult: function(callback) {
                baz.asyncCall(parameterFromFoo, 2, callback);
            }
        ],
            function(err, result) {
                if (err) {
                    handleError(err);
                } else {
                    thirdStep(result.barResult, result.bazResult);
                }
            }
    }
],
   function(err, result) {
       if (err) {
           handleError(err);
       } else {
           /* no-op? just assume third-step runs? */
       }
   }
);

Step was very focused and sequential, and my little draft here shows it getting messy in the adaptation. Am I missing something?

So my question is: What is the right way to convert the clear Step code into Async.JS? Or have I chosen the wrong library to upgrade to? I don't want my code to get uglier, but I don't want to depend on a library that seems kind of "dead", either. :-/

  • 1
    If you're switching libs, you might want to look at the Q promise library which is also pretty heavily used (it is the #11 most depended on thing in npm). Search for one of the user-group/conference talks by Dominic Denicola, one of the main authors for Q, on Youtube. You'll at least get a good overview and can see if it might better fit your worldview. I have found it works well for me. – barry-johnson Mar 03 '14 at 04:50
  • @barry-johnson Very interesting; looked through it. (And apparently his name is spelled "Domenic".) If you are familiar, might you by chance give me a leg up with an answer showing if it can be done as cleanly as the step model? What I'm not liking about async.js is that it requires putting callbacks for later parallel items ahead; it's messing up the code. A "Q perspective" which does what I'm looking to get would be appreciated...! – HostileFork says dont trust SE Mar 03 '14 at 16:30
  • Thanks for the name correction. I actually don't want to put any 'e's in his name, so am always correcting his last name as well. LOL - I will try to work an example up this afternoon or evening. Could you clarify what `this` is in your original step code? As it reads, I am guessing it's a callback function provided by step somehow and am assuming the `this.parallel()` is again a (specialized) callback to support fork/join sort of behavior. I'll glance at the step doc as well. – barry-johnson Mar 03 '14 at 16:41
  • In the meantime, if it might be helpful, I recently [answered a question](http://stackoverflow.com/questions/22109487/nodejs-mysql-dump/22110015#22110015) which included identically-functioning plain-callback and promise-enabled versions of some database code. I heavily commented the promises version so it might be as an illustration. – barry-johnson Mar 03 '14 at 16:44
  • @barry-johnson Yes, you read correctly; this.parallel is how step lets you start parallel calls, but it does not go to the next step until all the calls in the current step have finished. In my situation that is what I'd like... It's really about that fundamental thing where the code "grows down" instead of growing right", but I also want sequence ordering preserved...! – HostileFork says dont trust SE Mar 03 '14 at 17:25

2 Answers2

1

Be careful with your callbacks, here. The callback signature is:

callback(err, arg1, arg2 ...)

So if your foo.asyncCall were to call it with:

callback(result1, result2)

Then the whole async would mysteriously fail from that point. The correct callback for success should start with a null, e.g.

callback(null, result1, result2)

Here is the corrected code:

function thirdStep(argFromBar, argFromBaz, callback) {
    /* etc... */
    callback(null, result);
}

async.waterfall([
    function firstStepNoArgs(callback) {

        // error callback("failed").  First args is not null means failed
        // in case of error, it just goes straight to function(err, result)

        foo.asyncCall(callback);
    },
    function secondStep(argFromFoo, callback) {

        // argFromFoo come from previous callback (in this case, result1)

        async.parallel([
            barResult: function(callback) {
                bar.asyncCall(parameterFromFoo, 1, callback);
            },
            bazResult: function(callback) {
                baz.asyncCall(parameterFromFoo, 2, callback);
            }
        ],
            function(err, result) {
                if (err) {
                    // in case of error you should do callback(error),
                    // this callback is from secondStep(argFromFoo, callback).

                    // this will pass to final function(err, result).

                    handleError(err);

                } else {

                    // you need to do callback(null) inside thirdStep
                    // if callback is not called, the waterfall won't complete

                    thirdStep(result.barResult, result.bazResult, callback);
                }
            }
    }
],
   function(err, result) {
       if (err) {
           handleError(err);
       } else {

           // everything is executed correctly, 
           // if any step failed it will gone to err.

       }
   }
);
wayne
  • 3,410
  • 19
  • 11
  • +1 Wayne, for pointing out the parameter corrections. I guess my question is if I could use Async to make something a bit clearer like the original. Do I have to break my waterfall by putting the thirdStep above the secondStep, just because it's parallel? :-/ – HostileFork says dont trust SE Mar 03 '14 at 12:29
1

As requested, an implementation of what you are doing with promises. Just paste and run and you should get the idea. Bear in mind the top half of the code is setting up simulated functions so you can better follow along how this works. Someone will probably tell me I should have done this as a gist, which I may do as well.

var Q = require('q');

var foo ={},  bar ={}, baz = {};
//   let's mock up some of your objects with some asynch functions
//   using setTimeout for async completion
foo.asyncCall = function ( cb) {
    setTimeout(function(){ cb(null, 'promises'); },500);
};
bar.asyncCall = function ( arg1, arg2, cb) {
    setTimeout(function(){
        var result = arg1 + ' can be ' + arg2;
        cb(null, result);
    },1200);
};
//  going to add a will-always-fail function for example purposes
bar.asyncFailure = function (arg1, arg2, cb){
    setTimeout(function(){
        cb(new Error(arg1 +' offer decent error handling'), null);
    },2000);    // longer delay - simulate a timeout maybe
};

baz.asyncCall = function ( arg1, arg2, cb) {
    setTimeout(function(){
        var result = arg1 + ' are really ' + arg2;
        cb(null, result);
    },800);
};

//  set up promise-enbaled calls. Q.denodeify is an easy way to deal with any
//  standard node function with a final parameter being an (err,data) callback
//  If these are your own functions, you can also create your own promises, but
//  Q.nodeify is probably the fastest way to adapt existing code.

bar.promiseFailure = Q.denodeify(bar.asyncFailure);
bar.promiseCall = Q.denodeify(bar.asyncCall);
baz.promiseCall = Q.denodeify(baz.asyncCall);

//  this is your wrap up call ('thirdStep' in your code)
function allTogetherNow(arg1, arg2) {
    console.log(arg1 +'\n' + arg2);
};

// now we can have some fun
//  an example that will run to completion normally
//  Q.ninvoke is sort of a 'one-time' denodeify, it invokes a node-style function
//  and returns a promise

function example(){
    Q.ninvoke(foo,'asyncCall')
        .then( function (x) {
            return [bar.promiseCall(x, 'confusing at first'),
                    baz.promiseCall(x, 'awesome after that')]
        })
        .spread(allTogetherNow)
        .fail(function(e){console.log('Had an error')})
        .finally(function(){console.log('Calling no matter what from example')});

};
// sometimes things aren't entirely fun though, and there can be an error
function example2(){
    Q.ninvoke(foo,'asyncCall')
        .then( function (x) {
            return [bar.promiseFailure(x, 'confusing at first'),
                    baz.promiseCall(x, 'awesome after that')]
        })
        .spread(allTogetherNow)
        .fail(function(e){console.log(e)})
        .finally(function(){console.log('Calling no matter what from example2')});
};

example();
example2();

For those that don't want to bother running it, the output emitted is:

promises can be confusing at first
promises are really awesome after that
Calling no matter what from example
[Error: promises offer decent error handling]
Calling no matter what from example2
barry-johnson
  • 3,204
  • 1
  • 17
  • 19
  • Thanks Barry for not just writing the example...but testing it too! It's certainly a bit *"confusing at first"* compared to the Step code, but I am going to study it and try and grok the implications. I'll adapt the question to soliciting alternative libraries so I can accept this if it turns out to be better than Async! In the meantime, don't know your musical taste, but... ["all the promises we've been giving..."](https://www.youtube.com/watch?v=2hJJwXZr3UI) :-) – HostileFork says dont trust SE Mar 04 '14 at 01:23
  • First - you are very welcome and thanks for the song link - not bad at all; good summertime driving music. A couple of notes - if it's not immediately clear, the `spread` method basically turns the results from the prior array of promises into the parameters passed to the function passed to `spread`. Also, you can put multiple specific `fail` calls in a chain like this (`then().fail().then().fail()`) you can also just tack one on the end, and errors will propagate to it, which I find very convenient. FWIW, I was confused reading about promises until I just started to use them, then...clarity. – barry-johnson Mar 04 '14 at 01:39
  • As you seem quite a nice fellow let me extend an invitation to come chat with us about some interesting stuff ([current top-rated open-source recruitment ad on SO, even](http://meta.stackoverflow.com/ads/display/210389))... in [Rebol and Red](http://rebolsource.net/go/chat-faq)! New ideas are good to talk about, promises or languages or otherwise. :-) – HostileFork says dont trust SE Mar 04 '14 at 01:43
  • Thanks. I just popped the link. Intriguing. I'll check it out later this evening. – barry-johnson Mar 04 '14 at 01:47
  • I've put a [code review question up](http://codereview.stackexchange.com/questions/43837/) of my first attempt to take this on. I think I've got the basic idea... if you have any comments over there lemme know! – HostileFork says dont trust SE Mar 09 '14 at 03:26