Asynchronous Programming
TL;DR — Quick Summary
- Use
async/awaitwithtry/catchfor readable async code — it's synchronous-looking but non-blocking. - Use
Promise.all()to run independent async operations in parallel — not sequentially with multipleawaitcalls. - Use
Promise.allSettled()when you want all results even if some fail —Promise.all()rejects immediately on first failure. - Never use
awaitin a regularforloop over independent operations — usePromise.all(arr.map(fn))instead.
Lesson Overview
JavaScript runs on a single thread — it can only do one thing at a time. But web applications constantly need to do things that take time: fetching data from an API, reading files, waiting for user input. Asynchronous programming is how JavaScript handles these operations without blocking the main thread.
The evolution of async JavaScript: callbacks (original, error-prone) → Promises (chainable, better error handling) → async/await (syntactic sugar over Promises, reads like synchronous code). Modern code uses async/await almost exclusively, but understanding Promises is essential because all async operations return them under the hood.
Conceptual Deep Dive
The Event Loop: JavaScript uses an event loop. Synchronous code runs first (call stack). Async callbacks/Promise handlers queue up and run after the call stack is empty. This is why console.log after a setTimeout(fn, 0) still runs before the timeout callback.
Promises: Represent a future value — in one of three states: pending, fulfilled, or rejected. Key methods:
.then(onFulfilled): handle success.catch(onRejected): handle failure.finally(fn): always runsPromise.all(arr): wait for all — rejects if any failPromise.allSettled(arr): wait for all — gets all results regardlessPromise.race(arr): resolves/rejects with the first to finishPromise.any(arr): resolves with the first to succeed
async/await: async makes a function return a Promise. await pauses execution until the Promise resolves. Must be inside an async function. Error handling uses standard try/catch.
Implementation Lab
// Promise: represents a value available in the future
const fetchUser = (id) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id > 0) {
resolve({ id, name: 'Alice', email: 'alice@example.com' });
} else {
reject(new Error('Invalid user ID'ID'));
}
}, 1000);
});
};
// Promise chain
fetchUser(1)
.then(user => {
console.log('User:', user.name);
return fetchUser(2); // return next promise
})
.then(user2 => console.log('User2:', user2.name))
.catch(err => console.error('Error:', err.message))
.finally(() => console.log('Done'));
// ✅ async/await — same result, cleaner syntaxawait — same result, cleaner syntax
async function loadUsers() {
try {
const user1 = await fetchUser(1);
console.log('User:', user1.name);
const user2 = await fetchUser(2);
console.log('User2:', user2.name);
} catch (err) {
console.error('Error:', err.message);
} finally {
console.log('Done');
}
}// ❌ Sequential — waits for each to complete before starting nextfor each to complete before starting next
async function loadSequential() {
const user = await fetchUser(1); // wait 1s
const posts = await fetchPosts(1); // wait another 1s
const comments = await fetchComments(1); // wait another 1s
// Total: ~3 seconds3 seconds
return { user, posts, comments };
}
// ✅ Parallel — start all at once, wait for all to completefor all to complete
async function loadParallel() {
const [user, posts, comments] = await Promise.all([
fetchUser(1),
fetchPosts(1),
fetchComments(1)
]);
// Total: ~1 second (all run simultaneously)1 second (all run simultaneously)
return { user, posts, comments };
}
// Promise.allSettled — get all results, even if some failif some fail
async function loadWithFallback() {
const results = await Promise.allSettled([
fetchUser(1),
fetchUser(-1), // This will fail
fetchUser(3)
]);
results.forEach((result, i) => {
if (result.status === 'fulfilled') {
console.log(`User ${i}:`{i}:`, result.value.name);
} else {
console.error(`User ${i} failed:`{i} failed:`, result.reason.message);
}
});
}
// Real-world: fetch with error handling
async function getProduct(id) {
const response = await fetch(`/api/products/${id}`}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`{response.status}`);
}
const data = await response.json();
return data;
}Pro Tips — Senior Dev Insights
Create a reusable fetchJSON(url) wrapper that handles response.ok checking and JSON parsing — centralise your fetch error handling instead of repeating it everywhere.
For retrying failed requests: use a recursive async function with a delay — libraries like p-retry do this well, but a simple version takes 10 lines.
The async IIFE pattern lets you use await at the top level in older environments: (async () => { const data = await fetch(...); })().
Common Developer Pitfalls
await a(); await b(); when a and b don't depend on each other. Use Promise.all([a(), b()]) to run in parallel.await only works inside async functions — using it at the top level of a module requires 'top-level await' (supported in modern environments)..catch() or use try/catch with await.async on functions that don't need it — unnecessary async wrapping adds micro-task overhead and misleads readers about the function's nature.Interview Mastery
JavaScript is single-threaded — it executes one operation at a time. The event loop continuously checks if the call stack is empty, then moves items from the task queue to the stack. Async operations (setTimeout, fetch, Promises) are offloaded to browser APIs/Node.js internals. When they complete, their callbacks are queued. The event loop picks them up when the call stack is clear. This is why JavaScript can be non-blocking despite being single-threaded — async operations don't sit on the call stack waiting; they're handled elsewhere and their results are queued.
Promise.all takes an array of promises and resolves with all their values when all succeed — but rejects immediately if any one fails. Use it when all results are required and a single failure means the operation should fail. Promise.allSettled waits for all promises to complete regardless of success or failure, returning an array of { status: 'fulfilled'/'rejected', value/reason } objects. Use it when you want all results, even partial successes — for example, loading multiple optional dashboard widgets where some failing shouldn't block others.
async/await is syntactic sugar over Promises — under the hood, async functions return Promises and await pauses execution inside the async function until the Promise resolves. async/await reads like synchronous code (top-to-bottom), handles errors with familiar try/catch, and is easier to debug (stack traces are cleaner). Promise chains use .then()/.catch() which are fluent but can become nested ('promise hell') with complex conditional logic. Both approaches are equivalent in capability — async/await is strongly preferred for readability in modern code.
Hands-on Lab Exercises
Build a data fetching function using async/await that: fetches from an API, checks response.ok, parses JSON, handles errors gracefully, and shows/hides a loading state.
Convert a Promise chain (.then().then().catch()) to async/await — verify both produce identical results.
Promise.all — time it against sequential awaits and compare the duration.retry(fn, retries, delay) async function that retries a failing async function up to N times with a delay between attempts.Real-World Practice Scenarios
Promise.all. Show each section as it loads using Promise.allSettled.Promise.allSettled.Deepen Your Knowledge
Asynchronous Programming
TL;DR — Quick Summary
- Use
async/awaitwithtry/catchfor readable async code — it's synchronous-looking but non-blocking. - Use
Promise.all()to run independent async operations in parallel — not sequentially with multipleawaitcalls. - Use
Promise.allSettled()when you want all results even if some fail —Promise.all()rejects immediately on first failure. - Never use
awaitin a regularforloop over independent operations — usePromise.all(arr.map(fn))instead.
Overview
JavaScript runs on a single thread — it can only do one thing at a time. But web applications constantly need to do things that take time: fetching data from an API, reading files, waiting for user input. Asynchronous programming is how JavaScript handles these operations without blocking the main thread.
The evolution of async JavaScript: callbacks (original, error-prone) → Promises (chainable, better error handling) → async/await (syntactic sugar over Promises, reads like synchronous code). Modern code uses async/await almost exclusively, but understanding Promises is essential because all async operations return them under the hood.
Deep Dive Analysis
<p><strong>The Event Loop:</strong> JavaScript uses an event loop. Synchronous code runs first (call stack). Async callbacks/Promise handlers queue up and run after the call stack is empty. This is why console.log after a setTimeout(fn, 0) still runs before the timeout callback.</p><p><strong>Promises:</strong> Represent a future value — in one of three states: <em>pending</em>, <em>fulfilled</em>, or <em>rejected</em>. Key methods:</p><ul><li><code>.then(onFulfilled)</code>: handle success</li><li><code>.catch(onRejected)</code>: handle failure</li><li><code>.finally(fn)</code>: always runs</li><li><code>Promise.all(arr)</code>: wait for all — rejects if any fail</li><li><code>Promise.allSettled(arr)</code>: wait for all — gets all results regardless</li><li><code>Promise.race(arr)</code>: resolves/rejects with the first to finish</li><li><code>Promise.any(arr)</code>: resolves with the first to succeed</li></ul><p><strong>async/await:</strong> <code>async</code> makes a function return a Promise. <code>await</code> pauses execution until the Promise resolves. Must be inside an <code>async</code> function. Error handling uses standard try/catch.</p>
Implementation Reference
// Promise: represents a value available in the future
const fetchUser = (id) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id > 0) {
resolve({ id, name: 'Alice', email: 'alice@example.com' });
} else {
reject(new Error('Invalid user ID'));
}
}, 1000);
});
};
// Promise chain
fetchUser(1)
.then(user => {
console.log('User:', user.name);
return fetchUser(2); // return next promise
})
.then(user2 => console.log('User2:', user2.name))
.catch(err => console.error('Error:', err.message))
.finally(() => console.log('Done'));
// ✅ async/await — same result, cleaner syntax
async function loadUsers() {
try {
const user1 = await fetchUser(1);
console.log('User:', user1.name);
const user2 = await fetchUser(2);
console.log('User2:', user2.name);
} catch (err) {
console.error('Error:', err.message);
} finally {
console.log('Done');
}
}// ❌ Sequential — waits for each to complete before starting next
async function loadSequential() {
const user = await fetchUser(1); // wait 1s
const posts = await fetchPosts(1); // wait another 1s
const comments = await fetchComments(1); // wait another 1s
// Total: ~3 seconds
return { user, posts, comments };
}
// ✅ Parallel — start all at once, wait for all to complete
async function loadParallel() {
const [user, posts, comments] = await Promise.all([
fetchUser(1),
fetchPosts(1),
fetchComments(1)
]);
// Total: ~1 second (all run simultaneously)
return { user, posts, comments };
}
// Promise.allSettled — get all results, even if some fail
async function loadWithFallback() {
const results = await Promise.allSettled([
fetchUser(1),
fetchUser(-1), // This will fail
fetchUser(3)
]);
results.forEach((result, i) => {
if (result.status === 'fulfilled') {
console.log(`User ${i}:`, result.value.name);
} else {
console.error(`User ${i} failed:`, result.reason.message);
}
});
}
// Real-world: fetch with error handling
async function getProduct(id) {
const response = await fetch(`/api/products/${id}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
return data;
}Common Pitfalls
- •Sequential awaits for independent operations — <code>await a(); await b();</code> when a and b don't depend on each other. Use <code>Promise.all([a(), b()])</code> to run in parallel.
- •Forgetting that <code>await</code> only works inside <code>async</code> functions — using it at the top level of a module requires 'top-level await' (supported in modern environments).
- •Not handling rejected promises — unhandled rejections cause warnings in browsers and crashes in Node.js. Always attach <code>.catch()</code> or use try/catch with await.
- •Using <code>async</code> on functions that don't need it — unnecessary async wrapping adds micro-task overhead and misleads readers about the function's nature.
Hands-on Practice
- ✓Build a data fetching function using async/await that: fetches from an API, checks response.ok, parses JSON, handles errors gracefully, and shows/hides a loading state.
- ✓Convert a Promise chain (.then().then().catch()) to async/await — verify both produce identical results.
- ✓Build a parallel data loader using <code>Promise.all</code> — time it against sequential awaits and compare the duration.
- ✓Implement a simple <code>retry(fn, retries, delay)</code> async function that retries a failing async function up to N times with a delay between attempts.
Expert Pro Tips
Interview Preparation
Q: What is the event loop and how does it relate to async JavaScript?
Master Answer:
JavaScript is single-threaded — it executes one operation at a time. The event loop continuously checks if the call stack is empty, then moves items from the task queue to the stack. Async operations (setTimeout, fetch, Promises) are offloaded to browser APIs/Node.js internals. When they complete, their callbacks are queued. The event loop picks them up when the call stack is clear. This is why JavaScript can be non-blocking despite being single-threaded — async operations don't sit on the call stack waiting; they're handled elsewhere and their results are queued.
Q: What's the difference between Promise.all and Promise.allSettled?
Master Answer:
Promise.all takes an array of promises and resolves with all their values when all succeed — but rejects immediately if any one fails. Use it when all results are required and a single failure means the operation should fail. Promise.allSettled waits for all promises to complete regardless of success or failure, returning an array of { status: 'fulfilled'/'rejected', value/reason } objects. Use it when you want all results, even partial successes — for example, loading multiple optional dashboard widgets where some failing shouldn't block others.
Q: Explain the difference between async/await and Promise chains.
Master Answer:
async/await is syntactic sugar over Promises — under the hood, async functions return Promises and await pauses execution inside the async function until the Promise resolves. async/await reads like synchronous code (top-to-bottom), handles errors with familiar try/catch, and is easier to debug (stack traces are cleaner). Promise chains use .then()/.catch() which are fluent but can become nested ('promise hell') with complex conditional logic. Both approaches are equivalent in capability — async/await is strongly preferred for readability in modern code.
Simulated Scenarios
Extended Reading
MDN: Asynchronous JavaScript
https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous
JavaScript.info: Promises
https://javascript.info/promise-basics
JavaScript.info: async/await
https://javascript.info/async-await
© 2026 DevHub Engineering • All Proprietary Rights Reserved
Generated on March 7, 2026 • Ver: 4.0.2
Document Class: Master Education
Confidential Information • Licensed to User