1.6 Async JS & Event Loop
← Back to Chapter 1: JavaScript Fundamentals
JavaScript is a single-threaded language, meaning it can only execute one command at a time on its single call stack. Despite this, it handles non-blocking I/O, network requests, and animations with high performance thanks to the Event Loop and concurrency runtime.
1. JavaScript Concurrency Architecture
The JavaScript runtime consists of:
- Call Stack: Executes synchronous code frames in a Last-In, First-Out (LIFO) order.
- Web APIs / Node APIs: Multi-threaded host environment features (DOM events,
fetch(),setTimeout, file I/O). - Microtask Queue: High-priority queue for Promise resolutions (
.then(),.catch(),async/await),queueMicrotask, andMutationObserver. - Macrotask Queue (Callback Queue): Standard-priority queue for
setTimeout,setInterval,setImmediate, and UI rendering.
graph TD
subgraph Browser Engine
CS["Call Stack (LIFO)<br/>Currently executing code"]
WA["Web APIs<br/>fetch(), setTimeout, DOM"]
end
subgraph Queues
MQ["Microtask Queue<br/>Promises, queueMicrotask"]
TQ["Macrotask Queue<br/>setTimeout, setInterval"]
end
EL{"Event Loop"}
CS -- "Async Request" --> WA
WA -- "Resolved Promise" --> MQ
WA -- "Timer Expired" --> TQ
MQ --> EL
TQ --> EL
EL -- "When Call Stack is empty,<br/>flush all Microtasks first,<br/>then one Macrotask" --> CS
2. The Event Loop Cycle & Priority
The Event Loop continuously checks if the Call Stack is empty:
- Executes any synchronous code on the Call Stack until empty.
- Flushes all pending tasks in the Microtask Queue until it is completely drained (including microtasks queued by other microtasks).
- Executes one macrotask from the Macrotask Queue.
- Renders UI updates (if needed).
- Repeats the loop.
console.log("1. Synchronous Start");
setTimeout(() => {
console.log("4. Macrotask (setTimeout)");
}, 0);
Promise.resolve().then(() => {
console.log("3. Microtask (Promise)");
});
console.log("2. Synchronous End");
// Output Order:
// 1. Synchronous Start
// 2. Synchronous End
// 3. Microtask (Promise)
// 4. Macrotask (setTimeout)
3. Promises: Lifecycle & Combinators
A Promise represents an eventual completion (or failure) of an asynchronous operation and its resulting value.
Promise States
- Pending: Initial state, neither fulfilled nor rejected.
- Fulfilled: The operation completed successfully (
resolve(value)). - Rejected: The operation failed (
reject(reason)).
Chaining & Error Handling
function fetchUserData(userId) {
return fetch(`https://api.example.com/users/${userId}`)
.then(response => {
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
return response.json();
})
.catch(error => {
console.error("Network or parsing failed:", error);
throw error; // Re-throw to caller
})
.finally(() => {
console.log("Fetch attempt completed.");
});
}
Promise Combinator Methods
| Method | Description | Fails Fast? |
|---|---|---|
Promise.all([p1, p2]) | Resolves when all succeed. Rejects immediately if any reject. | Yes |
Promise.allSettled([p1, p2]) | Waits for all to complete (whether resolved or rejected). | No |
Promise.race([p1, p2]) | Settles as soon as the first promise settles (resolves or rejects). | Yes (if 1st fails) |
Promise.any([p1, p2]) | Resolves as soon as the first promise succeeds; rejects only if all fail. | No |
// Fetch multiple endpoints in parallel:
const [users, posts] = await Promise.all([
fetch("/api/users").then(r => r.json()),
fetch("/api/posts").then(r => r.json())
]);
4. async / await Syntax
async and await are syntactic sugar over Promises, making asynchronous code read sequentially like synchronous code.
- Any function marked
asyncautomatically returns a Promise. awaitpauses execution of theasyncfunction until the Promise settles, freeing the call stack to process other tasks.
async function loadDashboard() {
try {
const user = await fetchUserData(101);
const analytics = await fetchAnalytics(user.accountId);
return { user, analytics };
} catch (err) {
console.error("Failed to load dashboard:", err);
throw err;
}
}
5. React Asynchronous Patterns & Race Conditions
When fetching data inside a React component's useEffect, fast user interactions (such as changing dropdown filters) can cause responses to return out of order—a Race Condition.
Solution: Cancellation with AbortController
import React, { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const controller = new AbortController();
const { signal } = controller;
async function loadData() {
try {
setLoading(true);
const res = await fetch(`https://api.example.com/users/${userId}`, { signal });
const data = await res.json();
setUser(data);
} catch (err) {
if (err.name !== 'AbortError') {
console.error("Fetch error:", err);
}
} finally {
setLoading(false);
}
}
loadData();
// Cleanup: Abort pending request if userId changes or component unmounts
return () => {
controller.abort();
};
}, [userId]);
if (loading) return <p>Loading user profile...</p>;
return <div><h3>{user?.name}</h3></div>;
}
6. Summary Checklist
- JavaScript is single-threaded; asynchronous concurrency is coordinated by the browser/Node event loop.
- Microtasks (Promises) always take precedence over Macrotasks (
setTimeout). - Use
async/awaitwithtry...catchfor readable, maintainable asynchronous workflows. - Guard against race conditions and memory leaks in React by utilizing
AbortControllerin effect cleanups.