Well! If there's one take-away from this part, it's that async code using raw callbacks isn't easy and quite error-prone.

Which means, we'll get bugs. C'est la vie! But you don't have to resign yourself to it: when this happens, there's really no reason to limit yourself to clumsy console.log() calls in order to hunt down the cause.

Classic or "long" (async) stack traces?

When your async code goes haywire, the first thing you'll likely want to zero in on is the origin of its scheduling. Its call stack, in a way, except not the one it's actually running in just now: the one that triggered its being scheduled, and provided its starting parameters.

Trouble is, classic call stacks have no such notion; regardless of how or when async code got scheduled, they expose the call stack at the time of call, not the scheduling stack.

v8 and Chrome started keeping tabs on, let us say, "async transitions," through each step of async processing scheduling all the way back in 2014, when the "Async" checkbox showed up in the debugger's Call Stack view in Chrome. At the time of this recording, in June 2019, it's in v8 and any project based on it (so anything based on Chromium or Node.js) and in JavaScriptCore, Safari’s JS engine.

Async call stacks truly are a game changer. Here's a small historical example from early 2014, when Chrome shipped this feature.

On the left hand, the “actual” call stack at invocation time for this callback to a jQuery Ajax call. Note how short it is: it starts at the root level for Ajax response processing, which is the network level, and that's it.

On the right hand, you see the exact same scenario, but with "long" or "async" call stacks enabled. You can clearly see that we got there, in our postOnFail() function, due to an XHR call made inside jQuery's Ajax API, itself from a function of ours called submitData(), which was invoked inside a setTimeout() scheduled by our function retrySubmit(), apparently resulting from an earlier attempt in postOnFail(). We've got a lot more useful context here.

Even better: if you click anywhere in this stack, the debugger will show the scope states at the time in question. You're travelling back in time!

For a few years now this feature doesn't even have a checkbox in this view, it's enabled by default. DevTools Preferences let you disable it, but I'm hard-pressed to imagine why you'd want to do that.

In Chrome 33+, Opera 15+, Edge 19+ and Electron

On the browser and desktop side, lots of things are based on v8, which means they do have that feature.

It's been in Chrome since version 33 (as a reminder, Chrome stable right now is version 75, it's been some time), in Opera since version 15, when it adopted the Chromium basis and Blink engine, and in Edge in its current development version, version 19, for which we don't have an official release date yet, but I like to believe it'll ship in 2019. Edge just adopted Chromium as a foundation too, including the v8 engine. The Brave browser also relies on Chromium, so it's part of the gang.

Code example: 18-debugging-async-browser.js

Let's see how this goes in Chrome. This code listens for a click even on the document, which is a first async step. Its event handler schedules code through setTimeout(), a second async step, and the relevant callback then schedules a final one using requestAnimationFrame(), a third async step. What does the call stack have to say once in there?

Why yes, we do see the three async transitions!

It's also interesting to note that we also get this information in logs produced by the console.trace() calls or by log points, which is super neat, especially when you just want to inspect the stack without halting execution. (demo run)

(Back to slides)

In practice, a wide gamut of async transitions are tracked by this feature, covering most imaginable use cases, from timers and events to input/output to storage APIs.

Electron, a desktop app foundation based on Chromium and v8, naturally follows suit. Many highly popular desktop apps are based on Electron, such as Slack, GitHub Desktop, VS Code, Microsoft Teams and many, many more.

Not in Firefox and Edge < 19

On the Firefox side, I'm sorry to say that although their JS debugger is excellent, especially when it comes to leveraging sourcemaps, it currently doesn't feature async stack traces, to my dismay.

As for Edge, released versions so far were based on their own JS engine called Chakra, which doesn't have that either. But again, you can use the current beta version 19 in compatible mode and get your hands on sweet async stacks thanks to the underlying v8!

In Safari

Safari isn't doing too bad for once, as you can get async call stacks in there.

(Back to example 18 in Safari)

See for yourself (demo run). Now, I couldn't find any information online about what async schedulings are supported by this, but I like to believe the list is quite similar to the one in Chromium.

On the other hand, console.trace() doesn't expose that same information, and there are no log points as you would get with v8.

In Node.js

On the Node.js side, all recent versions support it through v8, just like in browsers. In practice, this became enabled by default with Node 10, the current LTS version, which will be maintained until April 2021. Even if you must constrain yourself to the Node versions supported by your hosting platform, you are guaranteed these days to have version 10 at a minimum, so no worries.

Code example: 19-debugging-async-node.js

Here's a tiny Node code similar to the one we tested earlier in browsers. No requestAnimationFrame() here but other asynchronous APIs exist that are supported as well, such as process.nextTick() and setImmediate().

Node’s console.trace() doesn't currently expose async transitions, but any debugger connected to Node will. You can for instance use a simple Chrome or Brave to debug your Node instance once its debugger is active (demo run). We get a lot of noise from Node's internals here, but we can tone that down using blackboxing, for instance by ignoring anything starting with /internals, then case-by-case on timers, _stream_readable and events, for instance.

(Back to slides)

On October 22, 2019, Node.js version 12 that shipped last April will become the active LTS, to be maintained until April 2022. It provides an extra command-line flag that helps things even further for the async/await syntax: we'll talk about it in the 3rd part of this screencast.

In VS Code?

Finally, VS Code lets us debug both Node and web pages right from the editor. For Node, this is built-in, and for browsers you just need to install the relevant debugger extension.

To get these async call stacks in VS Code, they need to be supported by the targeted JS engine, the one the editor connects to. So this won't magically add async call stack support to Firefox or Edge pre-19.

(Back to example 19 in VSCode)

Since this example here needs to read contents from the terminal, we can't just launch it with Code, as it will short-circuit its standard input. We must launch it outside, then use Code’s Attach to Process feature to latch our Node debugger onto it, which preserves the process' terminal access. This is a workflow totally specific to that example, you don't need to do that when debugging a web server, for instance.

Do note that Code sometimes automagically filter all of Node's internals and provide better labelling for things like process.nextTick(), for instance. The developer experience here is a few pegs above that of browsers, not to mention we have all the comfort of staying in our beloved editor.