1. Synchronous vs Asynchronous JavaScript
Line-by-Line Execution
Each statement must completely finish executing before the next line can run. A slow 5-second task freezes the whole page.
Delegated Background Execution
Long-running operations (API requests, timers) run in the background. The main thread continues running other code smoothly.
2. Why Do We Need Asynchronous JavaScript?
If network requests were synchronous, your entire website UI (buttons, animations, scrolling) would freeze solid while waiting for a server response. Asynchronous architecture ensures 60 FPS buttery smoothness while background data streams in.
3. The JavaScript Runtime Architecture
Tracks current active function execution frames (LIFO: Last In, First Out).
Browser background threads handling fetch(), setTimeout, and DOM events.
Holds completed asynchronous callbacks waiting for the Call Stack to empty.
Constantly checks if Call Stack is empty, then moves queued tasks into the stack.
4. Callbacks and Callback Hell
getUser(userId, (user) => {
getOrders(user.id, (orders) => {
getOrderDetails(orders[0].id, (details) => {
console.log(details);
});
});
});
5. Promises: The Solution to Inversion of Control
const success = true;
if (success) {
resolve({ name: "Rahul", id: 101 }); // 🟢 Fulfilled
} else {
reject(new Error("User not found")); // 🔴 Rejected
}
});
fetchUser
.then(user => console.log("User:", user.name))
.catch(err => console.error("Error:", err.message))
.finally(() => console.log("Operation complete"));
6. Modern async and await with try/catch
try {
const res = await fetch(`https://api.example.com/users/${userId}`);
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
const user = await res.json();
return user;
} catch (error) {
console.error("Failed to load user:", error.message);
}
}
7. Handling Multiple Operations: Promise.all()
const [users, products] = await Promise.all([
fetch("/api/users").then(r => r.json()),
fetch("/api/products").then(r => r.json())
]);
8. Fetching Data From an API with fetch()
The native fetch() API makes HTTP requests across the network. It returns a Promise that resolves to the Response object representing the stream.
9. Handling Loading and Error States
Show skeleton loaders or spinners while isLoading === true.
Render data components smoothly when data is received.
Display helpful retry alerts when network or 500 errors occur.
10. Practical Real-World Example
const response = await fetch(`https://api.github.com/users/${username}`);
if (!response.ok) throw new Error("GitHub user not found");
const profile = await response.json();
return profile;
}
11. Common Asynchronous Mistakes
- Forgetting
await: Trying to readres.datadirectly on a Promise object without awaiting it returnsundefined. - Sequential loops when parallel is possible: Using
awaitinside a standardforloop runs requests one-by-one; usePromise.all(arr.map(fn))instead. - Forgetting
try/catch: Uncaught promise rejections crash Node servers and trigger browser console error flags.
12. Async Clean Code Best Practices
- Prefer
async/awaitover chained.then()for clean linear readability. - Always check
response.okbefore callingresponse.json(). - Use
AbortControllerto cancel stale network requests on component unmount or rapid input changes.
13. Conclusion & Mental Model Summary
JavaScript executes code on a single Call Stack. When asynchronous Web APIs complete in background threads, the Event Loop schedules their results seamlessly without blocking user interaction!