1. The Single-Threaded Concurrency Model & The Event Loop#
One of the most consequential architectural attributes of JavaScript is that JavaScript is single-threaded. A single JavaScript runtime execution agent possesses exactly one Call Stack and can execute only one instruction at any given instant.
Yet, web applications and Node.js servers routinely handle thousands of concurrent network sockets, file I/O operations, database queries, and user input events without freezing. This non-blocking high-throughput concurrency is achieved through the Event Loop—an architectural collaboration between the JavaScript engine (V8, SpiderMonkey) and its host environment (the Browser platform or Node.js Libuv runtime).
+-------------------------------------------------------------------------+
| THE JAVASCRIPT ASYNCHRONOUS ARCHITECTURE |
+-------------------------------------------------------------------------+
| |
| +-----------------------+ +----------------------------+ |
| | CALL STACK (V8) | | HOST PLATFORM APIs | |
| | - Single Thread | | - Browser: fetch, DOM, | |
| | - Synchronous frames | | setTimeout, indexedDB | |
| | - LIFO execution | | - Node.js: Libuv Thread | |
| | | | Pool, epoll, kqueue, fs | |
| +-----------+-----------+ +-------------+--------------+ |
| | | |
| | Dispatches Async Task | Completes I/O |
| +--------------------------------------->| |
| | |
| v |
| +-------------------------------------------------------------------+ |
| | QUEUE INFRASTRUCTURE | |
| | | |
| | 1. Microtask Queue (HIGHEST PRIORITY) | |
| | Promise.then(), queueMicrotask(), process.nextTick() | |
| | | |
| | 2. Macrotask Queue (Task Queue) | |
| | setTimeout, setInterval, setImmediate, I/O events, UI clicks | |
| +-----------------------------------+-------------------------------+ |
| | |
| v |
| +-------------------------------------------------------------------+ |
| | THE EVENT LOOP TICK ARBITRATION | |
| | | |
| | Loop Check: | |
| | 1. Is Call Stack empty? | |
| | - NO --> Wait for current synchronous frame to finish. | |
| | - YES --> Proceed to Microtasks. | |
| | 2. DRAIN MICROTASKS: Run ALL pending microtasks until queue empty.| |
| | 3. Render Stage (Browser): Execute requestAnimationFrame & Paint. | |
| | 4. DEQUEUE ONE MACROTASK: Push 1 task to Call Stack and execute. | |
| | 5. Repeat cycle indefinitely. | |
| +-------------------------------------------------------------------+ |
| |
+-------------------------------------------------------------------------+The Event Loop Turn (Tick) Mechanics#
The Event Loop continuously executes a formal arbitration protocol:
- Synchronous Execution: Code on the Call Stack executes synchronously until the stack is completely empty.
- Microtask Queue Exhaustion (Drain):
- The engine checks the Microtask Queue.
- It dequeues and executes the first microtask.
- Crucial Rule: If executing that microtask queues another microtask, the newly queued microtask is executed in the same turn!
- The engine will never yield to rendering or macrotasks until the Microtask Queue is 100% empty.
- Rendering & Compositing (Browsers): If the display refresh interval is due (typically every 16.6ms for 60Hz displays), the browser runs
requestAnimationFramecallbacks, recalculates style, executes layout reflow, and paints pixels. - Macrotask Execution: The engine selects exactly one task from the Macrotask Queue, pushes its callback frame onto the Call Stack, and executes it.
- Once that single macrotask finishes, the engine immediately loops back to Step 1, draining any microtasks that were spawned before processing the next macrotask!
The Microtask Starvation Hazard#
Because microtasks drain exhaustively before any macrotask or UI render pass can occur, recursively spawning microtasks creates an infinite loop that freezes the entire operating system thread:
// CATASTROPHIC ENGINE LOCKUP: Microtask Starvation!
function infiniteMicrotasks() {
queueMicrotask(() => {
infiniteMicrotasks(); // Recursively queues microtask!
});
}
infiniteMicrotasks();In a browser, the tab freezes instantly: buttons cannot be clicked, CSS animations halt, and the tab crashes.
In Node.js, network I/O and timers freeze completely.
2. Macrotasks vs. Microtasks: The Exact Execution Hierarchy#
To write deterministic asynchronous code, an engineer must know which queue handles each asynchronous API:
+-------------------------------------------------------------------------+
| TASK QUEUE ARBITRATION & SCHEDULING MATRIX |
+---------------------+-------------------+-------------------------------+
| QUEUE TIER | CONCRETE APIS | DRAIN BEHAVIOR |
+---------------------+-------------------+-------------------------------+
| 1. process.nextTick | process.nextTick()| Drained immediately after |
| (Node.js Only) | | current operation, BEFORE any |
| | | other microtask runs. |
+---------------------+-------------------+-------------------------------+
| 2. Standard | Promise.then(), | Drained completely after Call |
| Microtask Queue | catch(), finally()| Stack empties, BEFORE any |
| | queueMicrotask() | macrotask or UI render. |
+---------------------+-------------------+-------------------------------+
| 3. Animation Queue | requestAnimation- | Executed prior to physical |
| (Browser Only) | Frame() | screen display repaint. |
+---------------------+-------------------+-------------------------------+
| 4. Macrotask Queue | setTimeout(), | Executed ONE AT A TIME. |
| (Task Queue) | setInterval(), | Microtasks drain after every |
| | setImmediate(), | individual macrotask! |
| | I/O socket events | |
+---------------------+-------------------+-------------------------------+Node.js Event Loop Phases (Libuv)#
In Node.js, macrotasks are further partitioned across 6 distinct phases managed by Libuv:
- Timers Phase: Executes callbacks scheduled by
setTimeout()andsetInterval(). - Pending Callbacks Phase: Executes I/O callbacks deferred to the next loop iteration (e.g., TCP socket errors).
- Idle, Prepare Phase: Internal Libuv housekeeping.
- Poll Phase: Retrieves new I/O events (incoming network connections, disk read completions); blocks and waits if no timers are scheduled.
- Check Phase: Executes callbacks registered via
setImmediate(). - Close Callbacks Phase: Executes socket close handlers (
socket.on('close', ...)).
Between every single phase, Node.js checks and drains process.nextTickQueue and the standard MicrotaskQueue!
3. Promise Architecture & State Machine Internals#
Before ES6 Promises, asynchronous JavaScript relied on callback functions passed down multiple layers. This produced the infamous "Callback Hell" (Pyramid of Doom), resulting in inversion of control, difficult error propagation, and unhandled failure states.
A Promise is an object representing the eventual completion (or failure) of an asynchronous operation and its resulting value. It is formalized under the Promises/A+ Specification.
+-------------------------------------------------------------------------+
| THE PROMISE FINITE STATE MACHINE (FSM) |
+-------------------------------------------------------------------------+
| |
| +-----------------+ |
| | PENDING | |
| | (Initial State) | |
| +--------+--------+ |
| | |
| +------------------+------------------+ |
| | | |
| v (resolve(value)) v (reject(error))|
| +-----------------+ +-----------------+ |
| | FULFILLED | | REJECTED | |
| | (Settled State) | | (Settled State) | |
| +-----------------+ +-----------------+ |
| |
| Irrevocability Invariant: |
| Once a Promise transitions from PENDING to FULFILLED or REJECTED, |
| its state is PERMANENTLY LOCKED. Subsequent calls to resolve() |
| or reject() are silently ignored. |
| |
+-------------------------------------------------------------------------+The Three Inviolable Promise Invariants#
- State Immutability: A promise exists in one of three states:
pending,fulfilled, orrejected. Once settled, it can never transition to another state, and its settled value cannot be mutated. - Asynchronous Execution Guarantee: Callbacks passed to
.then(),.catch(), or.finally()are guaranteed to execute asynchronously via the Microtask Queue, even if the Promise was already fulfilled synchronously before.then()was attached! - Chaining & Unwrapping: Every call to
.then()returns a brand-new Promise. If a handler returns a plain value, the new promise fulfills with that value. If a handler returns another Promise, the new promise "adopts" the state of the returned promise (recursive unwrapping).
// Synchronously fulfilled promise
const p = Promise.resolve(42);
p.then(val => console.log("Microtask output:", val));
console.log("Synchronous output");
// Console Output Order:
// 1. "Synchronous output"
// 2. "Microtask output: 42"The Four Promise Combinators#
Modern JavaScript provides four standardized static methods for managing concurrent promises:
+-------------------------------------------------------------------------+
| THE FOUR PROMISE COMBINATOR PARADIGMS |
+---------------------+-------------------+-------------------------------+
| COMBINATOR | RESOLVES WHEN | REJECTS WHEN |
+---------------------+-------------------+-------------------------------+
| Promise.all() | ALL promises | ANY promise rejects |
| | fulfill (Array) | (Fail-Fast: first rejection) |
+---------------------+-------------------+-------------------------------+
| Promise.allSettled()| ALL promises | NEVER REJECTS! |
| | settle (Fulfill | Returns array of {status, |
| | OR Reject) | value|reason} status objects |
+---------------------+-------------------+-------------------------------+
| Promise.race() | FIRST promise | FIRST promise rejects |
| | fulfills | (First settled promise wins) |
+---------------------+-------------------+-------------------------------+
| Promise.any() | FIRST promise | ALL promises reject |
| | fulfills | (Throws AggregateError) |
+---------------------+-------------------+-------------------------------+4. Async/Await: Syntactic Sugar over Generators & Microtask Coroutines#
Introduced in ES2017, the async and await keywords revolutionized asynchronous control flow. Rather than writing nested .then() callbacks, developers write asynchronous code that visually reads like linear, synchronous code.
How `async/await` Operates Under the Hood#
Underneath the syntax, *`async/await` is implemented as an automated Coroutine combining Generators (`function and yield`) with Promises**:
When an async function executes:
- It automatically wraps its entire return value in a
Promise. If the function returns a plain value42, it resolves asPromise.resolve(42). If it throws an error, it rejects asPromise.reject(err). - When the JavaScript engine encounters the `await` keyword:
- It pauses the execution of the local function.
- It evaluates the expression following await and wraps it in a Promise: Promise.resolve(expression).
- It attaches the remainder of the function as a callback in the .then() microtask continuation!
- It pops the function's execution context off the Call Stack, immediately returning control to the caller or the Event Loop.
- When the awaited Promise settles, the continuation is queued in the Microtask Queue, restoring the local variables and resuming execution.
async function fetchUserMetrics(userId) {
console.log("A: Synchronous start");
// Await pauses fetchUserMetrics, packages rest into microtask continuation
const res = await Promise.resolve({ id: userId, score: 98 });
console.log("B: Resumed in microtask:", res.score);
return res.score;
}
console.log("1: Script start");
fetchUserMetrics(101);
console.log("2: Script end");
// Exact Execution Order:
// 1. "1: Script start"
// 2. "A: Synchronous start"
// 3. "2: Script end"
// 4. "B: Resumed in microtask: 98"The Sequential Loop Disaster vs. Concurrent `Promise.all()`#
A common production anti-pattern is executing independent asynchronous operations sequentially inside a loop:
// CATASTROPHIC PERFORMANCE ANTI-PATTERN: Sequential Waterfall
// 10 items taking 200ms each = 2,000ms total latency!
async function fetchBatchBad(itemIds) {
const results = [];
for (const id of itemIds) {
const data = await api.fetchItem(id); // Pauses loop sequentially!
results.push(data);
}
return results;
}
// HIGH-PERFORMANCE CONCURRENT PATTERN
// 10 items executed in parallel = ~200ms total latency!
async function fetchBatchOptimized(itemIds) {
const promises = itemIds.map(id => api.fetchItem(id));
return await Promise.all(promises);
}5. Cancellation & Cooperative Concurrency: `AbortController` & `AbortSignal`#
A historical limitation of Promises is that they are not cancelable natively. Once a Promise is launched, it executes to completion even if the user navigates away from the page, clicks a "Cancel" button, or encounters a timeout.
To solve this, the W3C standardized the `AbortController` API, providing cooperative cancellation across network requests, timers, and custom worker pipelines.
+-------------------------------------------------------------------------+
| THE ABORTCONTROLLER CANCELLATION MODEL |
+-------------------------------------------------------------------------+
| |
| const controller = new AbortController(); |
| const signal = controller.signal; |
| |
| [ Consumer Component ] [ Asynchronous Worker ] |
| User clicks "Cancel Task" Receives signal in fetch/loop |
| | | |
| v | |
| controller.abort() | |
| | | |
| v v |
| Fires 'abort' event on signal ---------> Catches abort event |
| Throws DOMException |
| ("AbortError") immediately |
| Closes network socket |
| |
+-------------------------------------------------------------------------+Native Timeout Cancellation (`AbortSignal.timeout`)#
Modern JavaScript engines (Chrome 103+, Node.js 17.3+) provide static factory methods that simplify timeout handling:
async function fetchWithDeadline(url, timeoutMs = 3000) {
try {
// Automatically cancels if network takes longer than 3 seconds
const response = await fetch(url, {
signal: AbortSignal.timeout(timeoutMs)
});
return await response.json();
} catch (err) {
if (err.name === "TimeoutError") {
console.error(`Request timed out after {timeoutMs}ms`);
} else if (err.name === "AbortError") {
console.error("Request was explicitly aborted by user");
} else {
console.error("Network error:", err);
}
throw err;
}
}6. Industrial Implementation: Production Asynchronous Concurrency Limiter (Task Pool)#
When processing large-scale asynchronous tasks (e.g., migrating 5,000 files to S3, scraping 2,000 URLs, or indexing documents into a vector database), firing all promises with Promise.all() crushes operating system socket descriptors, triggers HTTP 429 Rate Limits, and causes Node.js memory exhaustion.
The following complete, standalone Node.js program demonstrates:
- A production-grade Concurrency Limiter (Task Pool) that caps maximum simultaneous active operations to
Nworkers. - An exponential backoff retry mechanism with random jitter.
- Full cooperative cancellation support via
AbortController.
'''
Production-Grade Asynchronous Task Pool & Concurrency Limiter.
Demonstrates event loop queueing, Promise coordination, and cooperative cancellation.
'''
// =====================================================================
// 1. CONCURRENCY LIMITER (PROMISE POOL)
// =====================================================================
class ConcurrencyLimiter {
constructor(maxConcurrency = 5) {
this.maxConcurrency = maxConcurrency;
this.activeCount = 0;
this.queue = [];
}
'''
Enqueues an asynchronous task generator function.
Resolves when the task executes within concurrency bounds.
'''
run(taskFn) {
return new Promise((resolve, reject) => {
this.queue.push({ taskFn, resolve, reject });
this.dispatch();
});
}
dispatch() {
// If under concurrency threshold and tasks remain in queue
while (this.activeCount < this.maxConcurrency && this.queue.length > 0) {
const { taskFn, resolve, reject } = this.queue.shift();
this.activeCount++;
// Execute task
taskFn()
.then(resolve)
.catch(reject)
.finally(() => {
this.activeCount--;
// Dequeue next task
this.dispatch();
});
}
}
getPendingCount() {
return this.queue.length;
}
getActiveCount() {
return this.activeCount;
}
}
// =====================================================================
// 2. RESILIENT TASK WITH RETRY & EXPONENTIAL BACKOFF + JITTER
// =====================================================================
async function executeWithRetry(operation, maxRetries = 3, baseDelayMs = 50, signal = null) {
let attempt = 0;
while (attempt < maxRetries) {
// Check if cancellation signal fired
if (signal && signal.aborted) {
throw new Error(`Operation aborted: {signal.reason}`);
}
try {
return await operation();
} catch (err) {
attempt++;
if (attempt >= maxRetries) {
throw new Error(`Failed after {maxRetries} attempts. Last error: {err.message}`);
}
// Calculate exponential backoff with full jitter
const expDelay = baseDelayMs * Math.pow(2, attempt);
const jitter = Math.random() * expDelay;
const totalWaitMs = expDelay + jitter;
console.warn(`[Retry Warning] Attempt {attempt} failed: {err.message}. Retrying in {totalWaitMs.toFixed(0)}ms...`);
// Sleep cooperative with abort signal
await new Promise((res, rej) => {
const timer = setTimeout(res, totalWaitMs);
if (signal) {
signal.addEventListener("abort", () => {
clearTimeout(timer);
rej(new Error(`Operation aborted during backoff: {signal.reason}`));
}, { once: true });
}
});
}
}
}
// =====================================================================
// 3. VERIFICATION HARNESS & BENCHMARK
// =====================================================================
async function main() {
console.log("=== ASYNCHRONOUS TASK POOL & CONCURRENCY BENCHMARK ===");
const TOTAL_TASKS = 20;
const MAX_CONCURRENT = 3;
const limiter = new ConcurrencyLimiter(MAX_CONCURRENT);
const controller = new AbortController();
const startTime = performance.now();
// Mock I/O operation
const createMockTask = (taskId) => async () => {
// Simulate network latency (between 60ms and 140ms)
const latency = Math.floor(Math.random() * 80) + 60;
// Simulate intermittent failure for Task 4 to demonstrate retry
if (taskId === 4 && Math.random() > 0.3) {
throw new Error("HTTP 503 Service Unavailable");
}
await new Promise(res => setTimeout(res, latency));
return { taskId, latency, completedAt: (performance.now() - startTime).toFixed(1) + "ms" };
};
console.log(`Submitting {TOTAL_TASKS} tasks to limiter (Max concurrency: {MAX_CONCURRENT})...\n`);
// Launch all tasks through limiter
const taskPromises = [];
for (let i = 1; i <= TOTAL_TASKS; i++) {
const task = createMockTask(i);
// Wrap with retry and concurrency bounds
const execution = limiter.run(() => executeWithRetry(task, 3, 40, controller.signal));
taskPromises.push(execution);
}
// Await all pooled tasks
const results = await Promise.allSettled(taskPromises);
const totalTime = (performance.now() - startTime).toFixed(2);
console.log(`\n=== ALL TASKS FINISHED IN {totalTime}ms ===`);
let succeeded = 0;
let failed = 0;
results.forEach(res => {
if (res.status === "fulfilled") succeeded++;
else failed++;
});
console.log(`Success: {succeeded} / Failed: {failed}`);
console.log("Sample Result Payload:", results[0]);
}
main();7. Real-World Architectural Case Studies#
Case Study 1: The Node.js Microtask Starvation Catastrophe in High-Scale Ingest APIs#
A telemetry gateway ingesting 100,000 JSON payloads per second ran into catastrophic unresponsiveness.
The Incident:
During traffic spikes, HTTP response latency jumped from 15ms to over 30 seconds. Health-check endpoints failed, and Kubernetes killed and restarted the pods, worsening the cascade.
Root Cause:
The engineering team used an internal recursive promise validator:
function validatePayloadBatch(records) {
if (records.length === 0) return Promise.resolve();
const item = records.shift();
return processItem(item).then(() => validatePayloadBatch(records));
}When batches arrived with 50,000 records, validatePayloadBatch chained 50,000 microtasks consecutively. Because the microtask queue was never empty, the Node.js Libuv poll phase could not run. The engine could not read incoming TCP packets or handle OS health-check pings, leading to orchestrator eviction!
The Architectural Fix:
Break long microtask chains by yielding to the macrotask queue every 100 items using setImmediate():
async function validatePayloadBatchSafe(records) {
let count = 0;
for (const item of records) {
await processItem(item);
count++;
if (count % 100 === 0) {
// Yield to macrotask queue: allows Libuv to process network I/O & health checks!
await new Promise(resolve => setImmediate(resolve));
}
}
}Case Study 2: The E-Commerce Checkout Waterfall Disaster#
A retail website suffered from a 4.2-second delay when customers clicked "Confirm Order".
The Post-Mortem Analysis:
The backend checkout handler was structured as:
// CATASTROPHIC WATERFALL
const user = await fetchUserProfile(userId); // 400ms
const cart = await fetchUserCart(userId); // 500ms
const discounts = await fetchActiveDiscounts(); // 350ms
const shippingRules = await fetchShippingRules(); // 450msEach service was independent, but the developer awaited them sequentially. Total latency was additive:
400 + 500 + 350 + 450 = 1,700ms just to gather data!
The Architectural Fix:
Execute all independent read operations concurrently via Promise.all():
const [user, cart, discounts, shippingRules] = await Promise.all([
fetchUserProfile(userId),
fetchUserCart(userId),
fetchActiveDiscounts(),
fetchShippingRules()
]);Total data acquisition latency dropped to the single slowest request: 500ms (a 70% latency reduction!).
8. Common Architectural Pitfalls & Anti-Patterns#
Pitfall 1: `await` inside `Array.prototype.forEach()`#
// CATASTROPHIC ASYNC BUG
async function notifyAllUsers(users) {
users.forEach(async (user) => {
// forEach does NOT await this promise!
await sendEmail(user.email);
});
console.log("All emails sent!"); // PRINTS IMMEDIATELY BEFORE ANY EMAIL IS SENT!
}forEach() expects a synchronous callback. It fires the async arrow functions and ignores their returned promises entirely. The function proceeds and prints "All emails sent" while all operations are still pending.
The Fix: Use for...of (for sequential) or Promise.all(users.map(...)) (for concurrent).
Pitfall 2: The Explicit Promise Construction Anti-Pattern#
// ANTI-PATTERN: Wrapping already-asynchronous APIs in 'new Promise()'
function fetchUserData(userId) {
return new Promise((resolve, reject) => {
fetch(`/api/users/{userId}`)
.then(res => res.json())
.then(data => resolve(data))
.catch(err => reject(err));
});
}
// CLEAN ARCHITECTURAL IDIOM: Return the promise chain directly!
function fetchUserDataClean(userId) {
return fetch(`/api/users/{userId}`).then(res => res.json());
}9. Checkpoint Practice Problems with Step-by-Step Solutions#
Problem 1: Complex Event Loop Execution Order Trace#
Analyze the following JavaScript snippet:
console.log("1");
setTimeout(() => {
console.log("2");
Promise.resolve().then(() => console.log("3"));
}, 0);
new Promise((resolve) => {
console.log("4");
resolve();
}).then(() => {
console.log("5");
setTimeout(() => console.log("6"), 0);
}).then(() => {
console.log("7");
});
console.log("8");Required Analysis:
- Detail the exact chronological sequence in which numbers are logged.
- Detail the exact contents of the Call Stack, Microtask Queue, and Macrotask Queue at each step.
Solution:
Step-by-Step Execution Trace:
console.log("1");executes synchronously on Call Stack.
- Logs: "1".
setTimeout(..., 0)registers a timer with the host platform.
- Macrotask Queue: [Task_A (logs 2, 3)].
new Promise(executor)executes synchronously immediately:
- console.log("4"); executes synchronously.
- Logs: "4".
- resolve() settles the promise to fulfilled.
.then(() => logs 5)is attached to a fulfilled promise:
- Queues microtask.
- Microtask Queue: [Microtask_1 (logs 5, schedules 6)].
console.log("8");executes synchronously on Call Stack.
- Logs: "8".
- Synchronous script execution finishes. Call Stack is empty!
- Drain Microtask Queue:
- Microtask_1 executes:
- Logs: "5".
- setTimeout(..., 0) registers a timer.
- Macrotask Queue: [Task_A, Task_B (logs 6)].
- Returns undefined, which schedules the chained .then():
- Microtask Queue: [Microtask_2 (logs 7)].
- The engine continues draining microtasks:
- Microtask_2 executes:
- Logs: "7".
- Microtask Queue is now completely empty!
- Dequeue First Macrotask:
- Task_A executes from Macrotask Queue:
- Logs: "2".
- Promise.resolve().then(...) queues a new microtask.
- Microtask Queue: [Microtask_3 (logs 3)].
- Task_A stack frame finishes.
- Drain Microtask Queue again:
- Engine must drain microtasks after every single macrotask!
- Microtask_3 executes:
- Logs: "3".
- Dequeue Next Macrotask:
- Task_B executes from Macrotask Queue:
- Logs: "6".
Final Verified Console Output:
"1"
"4"
"8"
"5"
"7"
"2"
"3"
"6"
Problem 2: Sequential vs. Concurrent Asynchronous Bottleneck Analysis#
A microservice processes user onboarding by calling three independent external APIs:
verifyIdentity(userId): Takes 350ms.createStripeCustomer(userId): Takes 450ms.provisionDatabaseSchema(userId): Takes 600ms.
An engineer writes:
async function onboardUser(userId) {
const idVerification = await verifyIdentity(userId);
const stripeCustomer = await createStripeCustomer(userId);
const dbSchema = await provisionDatabaseSchema(userId);
return { idVerification, stripeCustomer, dbSchema };
}Required Analysis:
- Calculate the total latency of
onboardUser(). - Refactor the function to execute concurrently and calculate the new total latency.
- If
createStripeCustomerfails with a network error, explain how error handling differs betweenPromise.allandPromise.allSettled.
Solution:
Part A: Sequential Latency:
Because each operation is preceded by await inside a sequential flow, their execution latencies are additive:
Total Latency = 350ms + 450ms + 600ms = 1,400ms.
Part B: Concurrent Refactoring & Latency:
async function onboardUserOptimized(userId) {
const [idVerification, stripeCustomer, dbSchema] = await Promise.all([
verifyIdentity(userId),
createStripeCustomer(userId),
provisionDatabaseSchema(userId)
]);
return { idVerification, stripeCustomer, dbSchema };
}All three network calls are dispatched simultaneously.
Total Latency = max(350ms, 450ms, 600ms) = 600ms.
Performance Gain: Latency drops from 1,400ms to 600ms (a 57% speedup).
Part C: Error Handling Behavior:
- With `Promise.all`: If
createStripeCustomerrejects at 450ms,Promise.allrejects immediately (fail-fast). The returned promise rejects with the Stripe error, and the results ofverifyIdentityandprovisionDatabaseSchemaare discarded. - With `Promise.allSettled`: It waits until all three operations finish (600ms). It never rejects. It returns an array of three objects where
idVerificationhas{ status: "fulfilled", value },stripeCustomerhas{ status: "rejected", reason }, anddbSchemahas{ status: "fulfilled", value }, allowing the application to initiate partial compensation workflows.
Problem 3: Custom Implementation of `Promise.allSettled()`#
Implement a robust, production-grade custom polyfill promiseAllSettled(promises) from scratch using native Promise primitives without using Promise.allSettled.
Solution:
function promiseAllSettled(promises) {
// Coerce input into array to handle non-array iterables
const items = Array.from(promises);
if (items.length === 0) {
return Promise.resolve([]);
}
// Wrap every item so that it ALWAYS resolves into a diagnostic descriptor object
const wrappedPromises = items.map(item => {
return Promise.resolve(item)
.then(value => ({
status: "fulfilled",
value: value
}))
.catch(reason => ({
status: "rejected",
reason: reason
}));
});
// Because all wrapped promises are guaranteed to fulfill, Promise.all will never reject!
return Promise.all(wrappedPromises);
}
// Verification Harness
const p1 = Promise.resolve("Data 1");
const p2 = Promise.reject(new Error("Network timeout"));
const p3 = 100; // Plain non-promise value
promiseAllSettled([p1, p2, p3]).then(results => {
console.log("allSettled Results:", results);
// Results:
// [
// { status: 'fulfilled', value: 'Data 1' },
// { status: 'rejected', reason: Error: Network timeout },
// { status: 'fulfilled', value: 100 }
// ]
});Problem 4: Cooperative Asynchronous Cancellation via `AbortController`#
Implement an asynchronous file chunk processor that:
- Iterates through an array of 1,000 binary chunks.
- Accepts an
AbortSignal. - If aborted, halts processing immediately, releases internal buffers, and throws a standard
DOMException("Operation aborted", "AbortError").
Solution:
async function processBinaryChunks(chunks, signal) {
const processedResults = [];
for (let i = 0; i < chunks.length; i++) {
// 1. Cooperative cancellation check before each chunk
if (signal.aborted) {
throw new DOMException("Operation aborted by client", "AbortError");
}
// 2. Process chunk
const chunk = chunks[i];
const checksum = await computeHashChunk(chunk);
processedResults.push({ chunkIndex: i, checksum });
// 3. Yield to Event Loop every 50 chunks
if (i % 50 === 0) {
await new Promise((resolve, reject) => {
const timer = setTimeout(resolve, 0);
// Listen for abort during sleep
signal.addEventListener("abort", () => {
clearTimeout(timer);
reject(new DOMException("Operation aborted by client", "AbortError"));
}, { once: true });
});
}
}
return processedResults;
}
// Mock hash function
function computeHashChunk(chunk) {
return Promise.resolve(chunk.length * 31);
}
// Verification Harness
const controller = new AbortController();
const mockChunks = new Array(500).fill("data_block");
// Trigger abort after 10ms
setTimeout(() => {
console.log("--> Triggering cancellation signal!");
controller.abort();
}, 10);
processBinaryChunks(mockChunks, controller.signal)
.then(res => console.log("Completed chunks:", res.length))
.catch(err => {
if (err.name === "AbortError") {
console.log("Caught expected cooperative AbortError:", err.message);
} else {
console.error("Unexpected error:", err);
}
});