From callbacks to async generators — parallel execution, AbortController, robust error handling, and production async code patterns.
Async programming is the foundation of every JavaScript application. Network requests, timers, user interactions, and database queries all operate asynchronously. Writing clean, correct async code is one of the most valuable skills in the JavaScript toolkit.
getUser(id, function(err, user) {
if (err) { handleError(err); return; }
getPosts(user.id, function(err, posts) {
if (err) { handleError(err); return; }
getComments(posts[0].id, function(err, comments) {
if (err) { handleError(err); return; }
renderPage(user, posts, comments);
});
});
}); // callback hell — repeated error handling, infinite nesting
getUser(id)
.then(user => getPosts(user.id))
.then(posts => getComments(posts[0].id))
.then(comments => renderPage(comments))
.catch(err => handleError(err)); // one catch for all
function wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function loadDashboard(userId) {
try {
const user = await getUser(userId);
const posts = await getPosts(user.id);
renderPage(user, posts);
} catch (error) {
handleError(error);
} finally {
hideSpinner(); // always runs
}
}
// Sequential: 600ms — each waits for the previous
const user = await getUser(id); // 200ms
const settings = await getSettings(id); // 200ms
const posts = await getPosts(id); // 200ms
// Parallel: ~200ms — all run simultaneously
const [user, settings, posts] = await Promise.all([
getUser(id), getSettings(id), getPosts(id)
]);
// allSettled: wait for all, never rejects
const results = await Promise.allSettled([fetch(a), fetch(b)]);
results.forEach(r =>
r.status === 'fulfilled' ? process(r.value) : log(r.reason)
);
// any: first SUCCESS wins
const data = await Promise.any([loadFromCache(k), fetchFromAPI(url)]);
class SearchService {
constructor() { this.controller = null; }
async search(query) {
if (this.controller) this.controller.abort();
this.controller = new AbortController();
try {
const res = await fetch(
'/api/search?q=' + encodeURIComponent(query),
{ signal: this.controller.signal }
);
return await res.json();
} catch (e) {
if (e.name === 'AbortError') return null; // expected
throw e;
}
}
}
async function* fetchAllPages(url) {
let nextUrl = url;
while (nextUrl) {
const res = await fetch(nextUrl);
const data = await res.json();
yield data.items;
nextUrl = data.nextPageUrl || null;
}
}
for await (const items of fetchAllPages('/api/products')) {
appendToUI(items);
}
Parallel execution with Promise.all, cancellation with AbortController, and streaming with async generators — these three patterns separate senior JavaScript developers from intermediate ones.