Understanding Asynchronous JavaScript: Callbacks, Promises, Generators, and Async/Await
This article explains why JavaScript needs asynchronous handling, describes the browser's multithreaded architecture and event‑loop, compares callbacks with Promises, introduces Generators for flow control, and shows how ES7 Async/Await combines their advantages with practical code examples.
Introduction
When you first learn JavaScript you discover that it runs on a single thread, is inherently asynchronous, and is suitable for I/O‑bound tasks but not CPU‑bound ones. However, many beginners never consider how asynchrony actually arises in their programs, why it appears, or alternative ways to handle it beyond simple callbacks.
Why Asynchrony Appears – Browser Kernel Multithreading
Browser threads
A modern browser typically has at least three resident threads: the JavaScript engine thread, the GUI rendering thread, and the event‑dispatch thread.
JS engine: event‑driven single‑threaded execution that continuously pulls tasks from the task queue.
GUI thread: performs repaint and layout (reflow) when the UI needs updating; it is mutually exclusive with the JS engine thread.
Event‑dispatch thread: when an event occurs (e.g., setTimeout, mouse click, AJAX response) it appends the event to the end of the task queue, where the JS engine will later process it.
Event Loop Mechanism
The task queue is divided into macro‑tasks and micro‑tasks. The loop works as follows:
Execute the current global context from top to bottom.
When an asynchronous event is encountered, delegate it to the appropriate browser subsystem.
The subsystem places macro‑tasks into the macro‑task queue and micro‑tasks into the micro‑task queue.
After the call stack empties, the engine processes the micro‑task queue first, then the macro‑task queue.
This process repeats, creating new empty macro‑ and micro‑task queues for newly generated asynchronous work until all queues are empty.
Handling Asynchrony – Callbacks
Callbacks are the simplest but not the most robust solution.
//callback typical usage
$.ajax({
url: 'www.pidanChen.com',
success(data){
console.log(data)
}
})
//delayed handling
try{
setTimeout(function(){
console.log('timeout')
},500)
}catch(e){
console.log('timerErr'+e)
}Problems with callbacks include:
Callback hell (order issues) when multiple dependent asynchronous operations are nested.
Inversion of control: third‑party APIs may invoke callbacks multiple times, out of order, or with unexpected arguments, making the caller lose control.
Promise – Restoring Trust
// create a Promise
let p = new Promise(function(res, rej){
if(err){
rej()
} else {
res()
}
})
p.then()A Promise receives a function with resolve and reject. The function runs immediately, and the returned Promise can be settled once. If the function throws, the Promise is rejected.
Promises reverse the inversion of control: the callback only notifies success, while the then handler, controlled by the Promise, performs subsequent actions.
Features: can be settled only once, with a single value; once settled, the state cannot change; each then callback runs exactly once.
Addressing Early, Late, or Missing Calls
Even an immediately resolved Promise is still asynchronous; its then callbacks run after the current call stack.
Promises never call callbacks too late; for timeout handling you can use Promise.race with a timeout Promise.
Because a Promise settles only once, extra calls are ignored.
// timeout helper
function timeoutPromise(delay){
return new Promise(function(res, rej){
setTimeout(function(){
rej('timeout')
}, delay)
})
}
Promise.race([doSomething(), timeoutPromise(3000)]).then().catch()Handling Errors
var p = new Promise(function(resolve, reject){
foo.bar(); // foo is undefined
resolve(2);
})
p.then(function(data){
console.log(data); // never reached
}, function(err){
console.log(err); // err is a TypeError from foo.bar()
});Generator – Solving Order Problems
Generators are functions that return iterators, declared with function*. Iterators expose a next() method that returns an object with value and done.
function* foo(){
var x = yield 2;
var y = x * (yield x + 1);
console.log(x, y);
return x + y;
}
var it = foo();
it.next() // {value: 2, done: false}
it.next(3) // {value: 4, done: false}
it.next(3) // {value: 12, done: true}Each yield pauses execution and can receive a value from the subsequent next() call, forming a two‑way communication channel.
Asynchronous Generator Example
// simulate ajax with setTimeout
var it = null;
function foo(){
let firstData = 'firstData';
setTimeout(function(){
it.next(firstData);
},500);
}
function goo(data){
let secondData = 'secondData';
setTimeout(function(){
if(data=='firstData'){
it.next(`${secondData}+${data}`);
}
},500);
}
function* main(){
let data = yield foo();
let newData = yield goo(data);
console.log('Result after two async steps:' + newData);
}
it = main();
it.next(); // start
// output: Result after two async steps:secondData+firstDataThis pattern lets us write seemingly synchronous code while the underlying operations remain asynchronous.
ES7 Solution – Async/Await
Async/Await is syntactic sugar over Generators and Promises: an async function automatically returns a Promise, and await pauses execution until the awaited Promise settles.
Built‑in executor: no need for external libraries like co.
Clearer semantics: async signals an asynchronous function, await signals a pause for a result.
How Async Works
An async function returns a Promise. If it returns a literal, the value is wrapped with Promise.resolve(). If it returns nothing, the Promise resolves to undefined.
What Await Waits For await can be used with any expression; if the expression evaluates to a Promise, await pauses until it resolves and yields the resolved value. If the expression is not a Promise, await returns the value immediately.
Typical Async/Await Scenario
function delayTime(n){
return new Promise(resolve => {
setTimeout(() => resolve(n + 200), n);
});
}
function step1(n){
console.log(`step1 with ${n}`);
return delayTime(n);
}
function step2(m, n){
console.log(`step2 with ${m} and ${n}`);
return delayTime(m + n);
}
async function main(){
console.time('doIt');
const time1 = 300;
const time2 = await step1(time1);
const time3 = await step2(time1, time2);
}
main();
// output:
// step1 with 300
// step2 with 300 and 500When multiple independent requests need to run in parallel, combine Promise.all with await:
async function doIt(){
let p1 = ajax(1);
let p2 = ajax(2);
let p3 = ajax(3);
await Promise.all([p1, p2, p3]);
clearLoading();
}Thus, Async/Await does not replace Promises; they complement each other and together form the most practical asynchronous solution for modern JavaScript.
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.
58 Tech
Official tech channel of 58, a platform for tech innovation, sharing, and communication.
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.
