Pattern for short circuiting async flows on non-errors in Node.js

Node uses the CPS convention for callbacks. Generally in the event a function returns an error the practice is to either handle the error or callback the error to your parent by doing something like return cb(err).

The issue I have is that often I need to short-circuit a pathway, but I don't actually have an "error" per-se. Is there an established pattern for doing this?

This issue comes up especially often when using async for flow control.

Here is an example:

var primaryFunction = function(arg1, cb) {
    var template;
    var data;
    var result; 

    async.series([
        function(cb) {
            // load some data from different sources
            async.parallel([
                function(cb) {
                    fs.readFile("/somepath", function(err, temp) {
                        if (err) { return cb(err); }

                        template = temp;

                        cb(null);
                    });

                },
                function(cb) {
                    api.find({}, function(err, temp) {
                        if (err) { return cb(err); }

                        data = temp;

                        cb(null);
                    });

                }
            ]
        },
        function(cb) {
            // do a bunch more work

            cb(null);
        },
        function(cb) {
            // do a bunch more work

            cb(null);
        },
        function(cb) {
            // combine data and template
            result = goatee.fill(template, data);

            cb(null);
        }
    ], function(err) {
        if (err) { return cb(err); }

        cb(null, result);
    });
}

Lets imagine if the api returns no documents, then I want to halt the flow control so it doesn't run any of the rest of that big async chain, but it's not exactly an error. It's just a short circuit out so maybe I'll put a friendly message on the screen and save myself some processing cycles.

Here are some options I consider:

  1. Pass a non-error such as cb("halt") and then check err instanceof Error in the final callback. The non-null value causes async to not run additional series. Then in my final callback I treat "real" errors one way (passing them up), and "my" errors my own way.
  2. I could create my own Error type and then return that and err instanceof MyError in the final callback. The issue here I've heard creating errors is expensive (certainly more expensive than a string or plain old {}).
  3. I could create a next style callback, but then the system does not play nicely with async and deviates from the CPS standard.
  4. I could use promises, since they provide a double callback idea with a success callback and a fail callback, but I'm just not a fan or promises since they aren't as universal as node CPS and there are too many competing implementations.
  5. I could set a variable to halt the state, but then I have to check that halt variable in every function in the asyn chain.

Right now, returning simple strings or simple objects seems the best, again this is for intra-module communications not communications that would ever reach outside my module. That solution "feels" dirty, but it technially works. Any ideas?

I would use option 2 myself. I'd create a HaltCondition error like so:

function HaltCondition(reason) {
    Error.call(this);
    this.message = reason;
}
util.inherits(HaltCondition, Error);

Then use that as the error parameter in the callback when I need to short circuit the rest of the async flow. I know you're concerned about the expense of creating an error object but, in my experience, it's negligible.

I threw together a quick test and profiled it to see what the difference would be between using the HaltCondition error and just returning true as the error instead (like your option 1).

This is the code I used (similar to the code in your question):

var async = require('async'),
    util = require('util');

function HaltCondition(reason) {
    Error.call(this);
    this.message = reason;
}
util.inherits(HaltCondition, Error);

async.series([
    function(cb) {
        async.parallel([
            function(cb) {
                setTimeout(function() {
                    cb(null, 'a');
                }, 10);
            },
            function(cb) {
                setTimeout(function() {
                    cb(null, 'b');
                }, 20);
            },
        ], cb)
    }, 
    function(cb) {
        setTimeout(function() {
            cb(new HaltCondition("test halt"), 'd');
        }, 50);
    },
    function(cb) {
        setTimeout(function() {
            cb(null, 'e');
        }, 30);
    }
], function(err, result) {
    if (err instanceof HaltCondition) {
        console.log('HALTED:', err.message);
    } else if (err) {
        console.log('ERROR:', err.message);
    } else {
        console.log('DONE:', result)
    }
});

I'm halting the async flow in the second series function.

When I was testing it with simply using true, I replaced the following lines:

cb(new HaltCondition("test halt"), 'd'); with cb(true, 'd');

and:

if (err instanceof HaltCondition) { with if (err === true) {

I profiled it in node v0.10.30 using node --profile and then processed v8.log with node-tick.

  • When using HaltCondition the code executed in an average of 93.2 ticks over 100 runs
  • When using true the code executed in an average of 93.3 ticks over 100 runs

An average of 0.1 ticks difference over 100 runs is basically nonexistent (and easily within the margin of error anyway).