Mastering JavaScript Asynchronous Programming: From Callbacks to async/await
This article explains JavaScript's single‑threaded nature, the task queue, macro‑ and micro‑tasks, the event loop, and compares callbacks, Promises, Generators, and async/await with code examples and practical pros and cons for front‑end developers.
Chen Chen, a front‑end engineer at WeDoctor Cloud Services, describes himself as a programmer whose "life lies in stillness".
Origin of Asynchrony
JavaScript runs on a single thread, meaning tasks are executed sequentially; a later task must wait for the previous one to finish.
When tasks are long‑running, such as network requests, timers, or event listeners, subsequent tasks are blocked, leading to unresponsive pages. Browsers solve this by using a task queue.
The browser creates separate threads for I/O, timers, and events, placing their callbacks into the task queue for the main thread to process.
Thus JavaScript achieves asynchronous behavior by distinguishing between synchronous and asynchronous tasks.
Synchronous tasks are queued and executed one after another. Asynchronous tasks are placed in the task queue and executed later when the event loop picks them up.
Asynchronous Execution Mechanism
Asynchronous tasks are divided into macro‑tasks and micro‑tasks.
MacroTask
Macro‑tasks are the standard tasks placed in the macro‑task queue, initiated by the browser host, such as:
script (the outer synchronous code)
setTimeout, setInterval, requestAnimationFrame
I/O operations
rendering events (DOM parsing, layout, painting)
user interaction events (clicks, scroll, zoom)
Macro‑tasks follow a FIFO order, but other system tasks may be inserted between them, which can affect efficiency.
MicroTask
Micro‑tasks fill the gaps between macro‑tasks, providing finer‑grained control. Examples include:
Promise callbacks (then, catch, etc.)
MutationObserver callbacks
When the JavaScript engine creates a global execution context, it also creates a micro‑task queue. Any micro‑tasks generated during a macro‑task are added to this queue and are executed before the next macro‑task begins.
Event Loop
The main thread runs JavaScript code using a call stack (LIFO). After all synchronous tasks in the stack finish, the event loop continuously pulls events from the task queue.
Execute a macro‑task (initially the outermost synchronous code). If a micro‑task is encountered, add it to the micro‑task queue.
After the macro‑task finishes, check for micro‑tasks. If present, execute step 3; otherwise, proceed to step 4.
Execute all micro‑tasks. New micro‑tasks generated during this process are also handled before moving on.
Check for the next macro‑task; if none, the loop ends.
Because micro‑tasks can enqueue additional micro‑tasks, an infinite micro‑task loop is possible. Developers must be careful to avoid recursive micro‑task creation.
History of Asynchronous Implementations
Callback Functions
A callback is a function passed as an argument to another function and executed after the latter completes (e.g., Ajax, I/O, timers).
console.log('setTimeout call before');
setTimeout(() => { console.log('setTimeout output'); }, 0);
console.log('setTimeout call after');
// Output:
// setTimeout call before
// setTimeout call after
// setTimeout outputsetTimeout callbacks are placed in the task queue and run after the main thread's synchronous code finishes.
Pros and Cons
Pros: Simple and easy to understand.
Cons: Leads to "callback hell", tightly coupled code, and difficult maintenance.
Promise
Promise, introduced in ES6, provides a container for the eventual result of an asynchronous operation.
Three states: pending, fulfilled (resolved), rejected; state is immutable once settled. then takes two callbacks—one for success, one for failure—and returns a new Promise. catch handles errors when the first then argument is null.
Additional APIs: finally, all, race, allSettled, any, resolve, reject.
new Promise((resolve) => {
resolve(step1());
}).then(res => {
return step2(res);
}).catch(err => {
console.log(err);
});When step1 and step2 are asynchronous, the resolved value of step1 is passed to then, which schedules the callback as a micro‑task.
Why Promise Callbacks Are Micro‑tasks
When the executor runs synchronously, resolve is called before any then is attached, leaving the callback list empty. To defer execution, a macro‑task (e.g., setTimeout) can be used, but browsers map Promise callbacks to the micro‑task queue for better performance.
Pros
Expresses asynchronous flow in a synchronous‑like manner, avoiding deep nesting.
Provides a unified interface for easier control of async operations.
Cons
Cannot be cancelled once created.
Uncaught errors inside a Promise are silent if no handler is attached.
While pending, progress cannot be observed.
Generator / yield
Generators, also introduced in ES6, allow pausing and resuming function execution using yield. They act as coroutine-like async containers.
Declared with function* syntax.
Inside, yield defines pause points and yields values. next() resumes execution and returns an object with value and done. next() can accept a value that becomes the result of the previous yield.
function* getData() {
let value1 = yield 111;
let value2 = yield value1 + 111;
return value2;
}
let meth = getData();
let val1 = meth.next(); // {value:111, done:false}
let val2 = meth.next(val1.value); // {value:222, done:false}
let val3 = meth.next(val2.value); // {value:222, done:true}Generators implement coroutines: the generator and its caller alternate execution on the main thread, offering high‑efficiency context switches.
Pros and Cons
Pros: Makes asynchronous flow appear sequential, improving readability.
Cons: Requires manual next() calls to advance.
async/await
async/awaitis syntactic sugar that wraps a Generator with an automatic executor, replacing yield with await and the Generator asterisk with async.
Built‑in executor; no manual next() needed. await can handle a Promise or a primitive (which is implicitly wrapped).
An async function always returns a Promise.
function sleep(time) {
return new Promise((resolve) => {
time += 1000;
setTimeout(() => resolve(time), 1000);
});
}
async function test() {
let time = 0;
for (let i = 0; i < 4; i++) {
time = await sleep(time);
console.log(time);
}
}
test();
// Output: 1000 2000 3000Pros and Cons
Pros: Cleaner and more straightforward than manual Generators.
Cons: Overusing await can block parallelism and hurt performance.
Multiple Async Execution Order Issues
The following example combines setTimeout, Promise, and async/await to illustrate execution order:
console.log('start');
setTimeout(function() { console.log('setTimeout'); }, 0);
async function test() {
let a = await 'await-result';
console.log(a);
}
test();
new Promise(function(resolve) {
console.log('promise-resolve');
resolve();
}).then(function() { console.log('promise-then'); });
console.log('end');
// Output:
// start
// promise-resolve
// end
// await-result
// promise-then
// setTimeoutExplanation of the steps:
Macro‑task (main script) starts – prints "start". setTimeout adds a macro‑task. test() runs; its await adds a micro‑task.
Promise executor runs synchronously – prints "promise-resolve". then callback is queued as a micro‑task.
Main script continues – prints "end".
First micro‑task runs – prints "await-result".
Second micro‑task runs – prints "promise-then".
Finally, the pending macro‑task runs – prints "setTimeout".
Conclusion
Front‑end developers frequently use asynchronous code. Understanding the underlying mechanisms and execution order helps write clearer, more efficient code and choose the appropriate abstraction (callbacks, Promise, Generator, or async/await) for different scenarios.
References
https://es6.ruanyifeng.com/
https://www.imooc.com/article/287901?block_id=tuijian_wz
http://www.ruanyifeng.com/blog/2012/12/asynchronous%EF%BC%BFjavascript.html
极客时间 - 李兵 (https://time.geekbang.org/column/intro/216)
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
WeDoctor Frontend Technology
Official WeDoctor Group frontend public account, sharing original tech articles, events, job postings, and occasional daily updates from our tech team.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
