The last couple of months I’ve been working in an application with a microservices architecture. I had to debug message errors like “Something went wrong with your request” with scarce and non-descriptive stack traces.
While trying to find those errors I learned a few things.

Stack this, stack that

The stack is the part of memory that keeps track at all times the functions invoked until a specific moment in your program. Think of a stack trace as a set of entries with an order of precedence, generally from top to bottom from most recent to older functions respectively.

Let’s make some nested function calls with the help of recursion and eval:

1
2
3
4
5
function fn(n){
if(n === 0) return Error().stack;
return eval(`(function fn${n}(){ return fn(n-1)})()`);
}
console.log(fn(10));

Running the previous example in Chrome (and Node.js or any V8-based runtime) will show you something like the following:

1
2
3
4
5
6
7
8
9
10
11
Error
at Error (native)
at fn (<anonymous>:2:24)
at fn1 (eval at fn (unknown source), <anonymous>:1:25)
at eval (eval at fn (unknown source), <anonymous>:1:34)
at fn (<anonymous>:3:12)
at fn2 (eval at fn (unknown source), <anonymous>:1:25)
at eval (eval at fn (unknown source), <anonymous>:1:34)
at fn (<anonymous>:3:12)
at fn3 (eval at fn (unknown source), <anonymous>:1:25)
at eval (eval at fn (unknown source), <anonymous>:1:34)

It’s being chopped up, Chrome defaults to a limit of 10 stack frames. To change the limit in Chrome, Node or Edge you can run Error.stackTraceLimit = Infinity.

In Firefox, the limit is 128 stack frames (at least a power of 2, right?) and it seems it’s being hardcoded. In Safari it’s not limited at all.

Long stack traces

Asynchronous stack traces, also called long stack traces, are something you will have to deal with if you write JavaScript. First, if you haven’t seen Philip Roberts’s talk “What the Heck is the Event Loop anyway” go and watch it now, I’ll wait.

Done? Ok, let’s continue.

By the nature of how things are brought up in the event loop, any async operation (timers or I/O) will have a local stack trace. See Node.js documentation on errors:

Stack traces extend only to either (a) the beginning of synchronous code execution, or (b) the number of frames given by the property Error.stackTraceLimit, whichever is smaller.

When debugging it’s common to want as much granularity as you can get to be able to identify the path to the line of code that has the error. No matter if it went through async operations to get executed.

That’s when long stack traces comes in: they are an extension of stack traces in an attempt of tracing the original source of async operations.

Longjohn is a useful npm package that will trace every asynchronous operation and will provide you with a complete stack trace (internally it monkeypatches every async operation with their own wrapper to do so).

Here’s how you can get long stack traces with Longjohn and Bluebird (just happens to be the one I use, could be any promises implementation):

1
2
3
4
5
6
7
require('longjohn');
const Promise = require('bluebird');
function example(){
Promise.resolve().then(() => console.log(Error().stack));
}

example();

Be aware that using long stack traces will have some performance overhead.

Note for Bluebird users: the long stack traces feature will work only when throwing. Example:

1
2
3
4
5
6
7
8
9
var Promise = require('bluebird');
Promise.longStackTraces();
function example(){
Promise.resolve().then(() => {
console.log(Error().stack); // no appereance of the function name
});
}

example();

Which is not good for tracing tools that use the state of the stack but do not throw.

What’s next?

It’s a (not well-known)problem the fact that we can’t get native promises (Node v4 or later) with asynchronous stack traces working. If you get an unhandledRejection error in your application and you’re using native promises, the only workaround to get an async stack trace is switching to another Promises implementation and use a package like longjohn.

And for now, it’s not clear how the problem of asynchronous stack traces with native promises it’s going to be solved, either for Node.js or in browsers. AsyncHooks sounds like a plausible solution for Node. I only know that I’ll be avoiding using native promises for some time.

Are you using other tools? Have you encountered other problems? Let me know in the comments.