The sideways stack trace

Stack trace from a series of asynchronous callsA common pattern in asynchronous programming is the callback pattern. Libraries such as async make use of that pattern to build asynchronous versions of common control flow structures. One unfortunate possible consequence of this is strange stack traces. For instance, here is code that executes five functions serially:

var functions = [
function one(done) {done();},
function two(done) {done();},
function three(done) {done();},
function four(done) {done();},
function five(done) {done();}
];

async.series(functions, function final() {
debugger;
});

The screenshot on the right shows the stack trace at the break point. As you can see, this trace is unreasonably deep, with lots of noise from the async library (that the IDE could offer to hide). What should really attract your attention however is the presence of all five functions in the call stack. That’s because each function is responsible for calling the next function in the series. As a result, we get this strange, “sideways” stack trace, where functions that are really executed one after the other linger on until the whole series is done calling. If there are lots of functions in the series, this could really become a problem. This is one of the reasons why Crockford and others are recommending that future versions of JavaScript implement tail calls as jumps.

As a point of comparison, here is similar synchronous code:

functions.forEach(function(f) {
f(function() {
debugger;
});
});

And here is the corresponding call stack, expectedly very short:

A much shorter synchronous stack trace

But wait. Some of you may have noticed something wrong with our “asynchronous” code: it’s not really asynchronous. It does use the callback pattern, but is really 100% synchronous. Could it be that those stack traces are an artifact of the callback pattern more then of asynchronous programming?

Let’s try it again with really asynchronous functions:

async.each(functions, function(f, next) {
process.nextTick(function() {
f(next);
});
}, function final() {
debugger;
done();
});

The stack trace for this code is indeed much more reasonable:Real asynchronous functions have better stack traces

So in the end, the toxic mix is callback-style APIs with synchronous implementations. Without going into premature optimizations, if you notice such call stacks in your applications, and determine they are a problem to its performance, or even to its ability to be easily debugged, I would recommend wrapping synchronous implementations of callback APIs inside process.nextTick calls. This will not only eliminate the stack problem, it will also yield flow control to the framework, making the application more responsive.

No Comments