From setTimeout: A Deep Dive into JavaScript's Asynchronous Execution Environment
Published: 2017-05-23
Problem background
In one development task, I needed to implement the following pie-chart animation drawn on a canvas. Because I did not understand the asynchronous mechanisms in the JS runtime, I encountered a tricky problem that I could not resolve. After discussing it with colleagues I finally realized the root cause. The issue boiled down to the classic JS timer-asynchronous problem. After fixing it, I read extensively and summarized practical experience; now I provide a deeper analysis of the asynchronous mechanisms in the JS runtime.

The image above shows the desired effect: each wedge should be drawn simultaneously and the circle should close.Click here to view the code listing. The problem I encountered earlier was that I didn’t extract myLoop as a function; instead I wrote all the logic, including the timer, directly inside the for loop. Although calculations for wedge angles and sentinel variables were correct, the circle never closed, which was very frustrating. I use this issue to introduce the importance of understanding the asynchronous mechanisms in the JS runtime; you don’t need to worry about the canvas drawing details here. The point is that understanding asynchrony affects the correctness of business logic, not just interview trivia. Why moving the timer logic into a function makes it work while putting it directly in the for loop fails will become clear after the detailed analysis below.
Deep dive into asynchrony
For a deeper treatment of asynchrony, I will provide as detailed and accurate an analysis as my current knowledge allows. You can read a blog post to learn about a debate between two experts on asynchrony. One is the well-known technical blogger Yuanyifeng (Ruan Yifeng); the other is Puling, a pioneer of Node in China. Both are idols I follow. This happened some time ago; I’ll just provide aarticle linkthat contains many of Puling’s annotations attached to Ruan’s post. Reading it will be very worthwhile and will also provoke broader technical reflections.
Synchronous and asynchronous
First, clarify the two concepts: synchronous and asynchronous.
12
f1()f2()
Regarding JavaScript execution modes, the runtime supports two modes: synchronous execution and asynchronous execution. For the two functions above, synchronous execution means calling f1 and waiting for its return before calling f2. Asynchronous means calling f1 and obtaining the expected result later through other operations, such as network or disk I/O. While those operations run, the program can continue and call f2 without waiting for f1’s result.
We know most scripting and programming languages use synchronous programming, which is easy for developers to reason about. Why does JS rely on asynchronous programming so often? This traces back to JS’s original host environment—the browser.
Because JS in browsers manipulates the DOM, it must be single-threaded to ensure DOM safety (for example, preventing one thread from deleting a DOM node another thread is using). With a single thread, synchronous long-running operations would make the browser unresponsive and give a poor user experience. If long tasks like AJAX requests run asynchronously, the client rendering won’t be blocked by those tasks.
On the server side, async execution in JS is even more important because the runtime is single-threaded. If all concurrent requests were handled synchronously, server responsiveness would be poor and performance would collapse. Asynchronous patterns are necessary to handle heavy concurrency, unlike Java or PHP which address concurrency with multiple threads. In today’s high-concurrency world, this became an advantage for JS and helped Node quickly enter the mainstream as a strong solution for I/O-bound applications.
Mechanisms that implement asynchrony
Before explaining mechanisms for implementing asynchrony, we must distinguish two concepts: the JavaScript execution engine and the execution environment. The V8 VM is commonly called the JS execution engine; Safari’s JavaScriptCore and Firefox’s SpiderMonkey are other engines. Browsers and Node are the JS execution environments, or runtimes. The engine implements the ECMAScript spec, while the runtime implements concrete asynchronous mechanisms. So when we talk about JS asynchronous mechanisms, we mean the runtime’s mechanisms, not the engine like V8; implementations are provided by browser vendors and other runtime providers.
Ways to implement asynchrony include the Event Loop, polling, and events. Polling is like repeatedly asking a waiter if your food is ready after you pay. Events are like paying once and waiting for the waiter to notify you when the food is ready. Most environments implement asynchrony via the Event Loop, so I’ll focus on that.
Event Loop
The Event Loop works as illustrated below. When a program starts, memory is divided into the heap and the stack; the stack holds the memory needed for the main thread’s execution logic, which we abstractly call the execution stack. Code on the stack calls various WebAPIs—for DOM operations, AJAX requests, creating timers, etc. These operations generate events, which are associated with handles (the registered callbacks) and are placed into the callback queue (event queue) in a queued structure. When the execution stack’s code finishes, the main thread reads the callback queue and executes callbacks sequentially, then proceeds to the next event loop tick to handle newly generated callbacks. Thus, code on the execution stack always runs before callbacks in the callback queue.

Callbacks registered with setTimeout() and setInterval() are classic examples of the Event Loop mechanism. Similarly, after the execution stack finishes, the event loop checks whether the system time has reached scheduled times; when it does, a timeout event is generated and placed in the callback queue for the next loop. In practice, if the execution stack takes a long time, callbacks from setTimeout() may not fire at the exact scheduled moment, so JS timers cannot guarantee strict precision. Understanding their characteristics allows us to optimize at the application level to make callback firing times closer to expectations. Since setTimeout() and setInterval() are essentially similar, the examples below will use setTimeout() to analyze async behavior.
Asynchronous programming
My understanding of asynchronous programming is implementing overall flow control in an asynchronous style on top of the runtime’s async mechanisms. Concretely, you can use callbacks like those in setTimeout(), event-driven publish/subscribe, ES6’s Promise as a unified async interface, ES7’s Async/Await, or libraries from the Node community such as Step for flow control. This defines the category of asynchronous programming; I won’t go deeper into usage patterns here.
Example analysis
In this section I’ll use multiple examples to analyze; combine them with the theories above to understand JS sync and async. We start with a classic JS async interview question and then get deeper.
1234567
for (var i = 0; i
The result of the code snippet above should be that it immediately outputs a 5, then after 1 second outputs five 5s simultaneously. When the program starts it first runs the synchronous code on the execution stack and almost simultaneously creates five timers, then continues with the synchronous code on line 7. So it first logs a 5, and after 1s the five timers each generate timeout events placed into the callback queue. The event loop executes the queued callbacks in order; due to closure behavior, each timer’s callback is bound to the for loop’s i variable in the defining context, and i has become 5 by that time, so it outputs five 5s.
If we now need the program to immediately output a 5, and then after 1s output 0,1,2,3,4 simultaneously, how would we modify the code above?
1234567891011121314151617181920212223
// Method onefor (var i = 0; i The two methods above actually share the same idea: use JS function scope as an independent scope to preserve a local context and bind it to the setTimeout callback via closure. The first uses an IIFE; the second defines a function and calls it for each iteration. At this point you should see this is the same issue described in the problem background.
Next, deepen the requirement: how do we output 0 immediately when the code runs, then output 1,2,3,4 every 1s in sequence, and after the loop ends output 5 at around the 5-second mark?
Because the repeated 0,1,2,3,4 outputs correspond to five timers (five async operations), we can abstract the requirement as: do something after a series of asynchronous operations complete (one async operation per loop). Those familiar with ES6 will think of Promise.
12345678910111213141516171819
const tasks = []; // store the Promises for async operations const output = (i) => new Promise((resolve) => { setTimeout(() => { console.log(new Date, i); resolve(); }, 1000 * i);}); // generate all async operations for (var i = 0; i { setTimeout(() => { console.log(new Date, i); }, 1000);});
If you are familiar with ES7’s Async/Await, you can also try solving this with that approach.
1234567891011121314
// Simulate sleep from other languages; in practice this can be any async operation
const sleep = (timeoutMS) => new Promise((resolve) => { setTimeout(resolve, timeoutMS);}); (async () => { // immediately-invoked async function expression for (var i = 0; i
Note you should pay attention to browser support for Async/Await; if your browser is not among the supported versions below, upgrade or use Babel to transpile.
If you understand the examples above, you’ll gain new insight into JS asynchrony. The next example further examines the timing of callback execution in async code.
123456789101112131415161718192021
let a = new Promise( function(resolve, reject) { console.log(1) setTimeout(() => console.log(2), 0) console.log(3) console.log(4) resolve(true) })a.then(v => { console.log(8)}) let b = new Promise( function() { console.log(5) setTimeout(() => console.log(6), 0) }) console.log(7)
First clarify that Promise is an ES6 API standard for asynchronous programming, but its constructor executes synchronously. So when you new a Promise, the constructor’s logic runs synchronously. Therefore the snippet above runs synchronous code top to bottom, outputting 1,3,4,5,7. Which runs first: the then callback or the setTimeout callbacks? Remember the former runs before the latter, so the later output is 8,2,6. That’s because an immediately resolved Promise’s handlers run at the end of the current event loop tick—similar to Node’s process.nextTick—allowing callbacks to run at the tail of the current execution stack, before the next Event Loop tick where the task queue is processed. setTimeout(fn, 0) schedules a task at the tail of the current task queue, meaning it runs in the next Event Loop tick, similar to Node’s setImmediate.
Finally, an example on optimizing setInterval. We know setTimeout callbacks are not precise because when the callback’s scheduled time arrives the execution stack may still be busy, preventing timely CPU scheduling for callbacks in the callback queue. setInterval has its own problems: intervals may be skipped or run with spacing that is shorter than the set interval. These issues are caused by other code holding the CPU for long slices. Consider the code below:
1234567
function click() { // code block1... setInterval(function() { // process ... }, 200); // code block2 ...}
If process takes too long—say it exceeds 400ms—the JS runtime may skip an intermediate interval because the callback queue only allows one instance of process to exist, causing inaccurate triggering.
To avoid this, we can optimize using recursion. Below are two implementations; I recommend the first. The second uses arguments.callee, which is forbidden in strict mode—ES5 disallows arguments.callee when a function must call itself. Instead, give the function expression a name or use a function declaration (see MDN explanation).
12345678910111213
// Implementation one setTimeout(function bar (){ // processing foo = setTimeout(bar, 1000); }, 1000); // Implementation two setTimeout(function(){ // processing foo = setTimeout(arguments.callee, interval); }, interval); clearTimeout(foo) // stop the loop
Last updated