Promises, Async/Await & Event Loop
Demystify the engine driving modern asynchronous JavaScript. Master Promise states, chaining, async/await syntax, and the definitive architectural mental model: the Event Loop, Call Stack, Microtask Queue, and Task Queue.
The Event Loop: Microtasks vs. Macrotasks Execution Engine
The fundamental rule of JavaScript concurrency: The Event Loop drains ALL pending Microtasks (Promises) before picking even ONE single Macrotask (setTimeout)!
1. The Promise Architecture
A Promise represents the eventual completion (or failure) of an asynchronous operation and its resulting value. Unlike callbacks, a Promise provides three foundational guarantees:
1. Pending
2. Fulfilled (Resolved)
.then() handlers.3. Rejected
.catch() handlers.Pending to either Fulfilled or Rejected, it is settled and permanently immutable. Calling resolve() or reject() a second time has zero effect, completely eliminating the Inversion of Control double-charge hazard!
Constructing and Consuming a Promise
function fetchOrder(orderId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (orderId <= 0) {
return reject(new Error('Invalid Order ID'));
}
resolve({ id: orderId, total: 249.99, status: 'PAID' });
}, 200);
});
}
// Chaining .then(), .catch(), and .finally():
fetchOrder(101)
.then(order => {
console.log('Order received:', order.total);
return order.id; // Return value is wrapped in a new fulfilled Promise!
})
.then(id => {
console.log('Processing confirmation for order #', id);
})
.catch(err => {
console.error('Error occurred in pipeline:', err.message);
})
.finally(() => {
console.log('Cleanup: Operation completed regardless of success or failure.');
});
Try it in Playground
2. async / await: Synchronous Elegance
Introduced in ES2017, async / await is syntactic sugar built on top of native Promises, allowing asynchronous code to be structured with clean, linear readability:
Production async / await Flow
async function processCustomerCheckout(userId, cart) {
try {
// 1. Await pauses this function's execution without blocking the browser thread:
const user = await fetchUserProfile(userId);
const invoice = await createInvoice(user, cart);
const payment = await chargePayment(invoice.total);
return { success: true, paymentId: payment.id };
} catch (err) {
// Standard structured try...catch catches ANY rejection in the sequence!
console.error('Checkout failed:', err.message);
throw err; // Re-throw to caller
} finally {
console.log('Checkout flow completed.');
}
}
Try it in Playground
Interactive Mini-Lab: Microtask vs. Macrotask Execution Tracer
Predict the output order of the following snippet, then click "Step Through Event Loop" to watch the V8 engine process synchronous frames, microtasks, and macrotasks in real time:
setTimeout(() => console.log('2: Macrotask setTimeout'), 0);
Promise.resolve().then(() => console.log('3: Microtask Promise 1'))
.then(() => console.log('4: Microtask Promise 2'));
queueMicrotask(() => console.log('5: Microtask queueMicrotask'));
console.log('6: Sync End');
Hands-on Challenge: Exponential Backoff Retry Utility
Coding Challenge
Real-world microservices experience transient network hiccups. Write an asynchronous retryWithBackoff(fn, maxRetries, baseDelayMs) function that automatically retries failed asynchronous calls with exponential backoff delay (e.g. 100ms → 200ms → 400ms):
- Use
async/awaitsyntax with cleantry...catchloops. - If
fn()succeeds, resolve immediately with its return value. - If
fn()fails, waitbaseDelayMs * (2 ** attempt)before retrying. - If all retries are exhausted, throw the final captured error.
/**
* Helper to pause execution asynchronously using Promise + setTimeout:
*/
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
/**
* Executes an async operation with automated exponential backoff retries.
*/
async function retryWithBackoff(asyncFn, maxRetries = 3, baseDelayMs = 200) {
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await asyncFn();
} catch (err) {
lastError = err;
if (attempt === maxRetries) break;
const delay = baseDelayMs * (2 ** attempt);
console.warn(`[Attempt ${attempt + 1} Failed] Retrying in ${delay}ms... Error: ${err.message}`);
await sleep(delay);
}
}
throw new Error(`All ${maxRetries} retries exhausted. Final error: ${lastError.message}`);
}
// Verification:
let callCount = 0;
const flakyApi = async () => {
callCount++;
if (callCount < 3) throw new Error('503 Service Unavailable');
return { status: 200, data: 'Mission Success!' };
};
retryWithBackoff(flakyApi, 4, 100).then(res => console.log('Resolved:', res));
Chapter 25 Knowledge Check
Validate your mastery of Promises, async/await, and Event Loop queue scheduling.
async function returns a primitive value like return 42;?Promise.resolve(42).
TypeError because async functions must return an object.
await is called on the outside.
console.log('A'); Promise.resolve().then(() => console.log('B')); setTimeout(() => console.log('C'), 0); console.log('D');?resolve() again updates the fulfilled value.
.finally() block.
await fetch(...) is encountered inside an async function, what happens to the main browser thread?