Async/Await in JavaScript: From Callbacks to Clean Code
JavaScript's evolution from callback hell to clean async code.
The Evolution
Stage 1: Callbacks (The Dark Ages)
// Callback hell: nested, hard to read, error-prone
getUser(id, (user) => {
getPosts(user.id, (posts) => {
getComments(posts[0].id, (comments) => {
getAuthor(comments[0].authorId, (author) => {
console.log(author.name);
// Error handling? Good luck.
});
});
});
});
Stage 2: Promises (Better)
// Flat chain but still verbose
getUser(id)
.then(user => getPosts(user.id))
.then(posts => getComments(posts[0].id))
.then(comments => getAuthor(comments[0].authorId))
.then(author => console.log(author.name))
.catch(err => console.error('Something failed:', err));
Stage 3: Async/Await (Modern)
// Reads like synchronous code!
async function getAuthorName(id) {
try {
const user = await getUser(id);
const posts = await getPosts(user.id);
const comments = await getComments(posts[0].id);
const author = await getAuthor(comments[0].authorId);
return author.name;
} catch (err) {
console.error('Failed:', err);
throw err;
}
}
How Async/Await Works
Code execution:
async function example() {
console.log('1'); // Runs immediately
const data = await fetchData(); // PAUSES here, returns to event loop
console.log('3'); // Runs AFTER data arrives
}
console.log('2'); // Runs while waiting for fetchData
// Output: 1, 2, 3
Timeline:
──── 1 ──── await fetchData() ────── 3 ────
↓ ↓
(paused) (resumed when promise resolves)
──── 2 ──── (runs during the wait)
Core Patterns
Pattern 1: Sequential Execution
// One after another (slow — each waits for the previous)
async function processUsers() {
const user1 = await getUser(1); // 1 second
const user2 = await getUser(2); // 1 second
const user3 = await getUser(3); // 1 second
// Total: 3 seconds
}
Pattern 2: Parallel Execution
// All at once (fast!)
async function processUsers() {
const [user1, user2, user3] = await Promise.all([
getUser(1),
getUser(2),
getUser(3),
]);
// Total: 1 second (all run concurrently)
}
Pattern 3: Parallel with Error Handling
// Get results that succeeded, collect errors
async function fetchMultiple(urls) {
const results = await Promise.allSettled(
urls.map(url => fetch(url).then(r => r.json()))
);
const succeeded = results.filter(r => r.status === 'fulfilled').map(r => r.value);
const failed = results.filter(r => r.status === 'rejected').map(r => r.reason);
console.log(`${succeeded.length} succeeded, ${failed.length} failed`);
return succeeded;
}
Pattern 4: Timeout Wrapper
// Reject promise if it takes too long
function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms)
);
return Promise.race([promise, timeout]);
}
// Usage
const data = await withTimeout(fetch('/api/data'), 5000);
// If fetch takes > 5 seconds, throws "Timeout after 5000ms"
Pattern 5: Retry with Backoff
async function fetchWithRetry(url, retries = 3, delay = 1000) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (err) {
if (i === retries - 1) throw err;
console.warn(`Attempt ${i + 1} failed, retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
delay *= 2; // Exponential backoff
}
}
}
Pattern 6: Async Iteration
// Process items one at a time (don't overwhelm the server)
async function processItems(items) {
for (const item of items) {
await processItem(item); // Wait for each to complete before next
}
}
// Process in batches
async function processInBatches(items, batchSize = 5) {
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
await Promise.all(batch.map(processItem));
console.log(`Processed batch ${Math.floor(i / batchSize) + 1}`);
}
}
Common Mistakes
❌ Forgetting await
async function getData() {
const data = fetchData(); // Missing await!
console.log(data); // Promise object, not the data!
}
// ✅ Fix: Always await
async function getData() {
const data = await fetchData();
console.log(data); // The actual data
}
❌ Using await in loops unnecessarily
// Slow: Sequential
for (const id of [1, 2, 3, 4, 5]) {
await fetchUser(id); // Each waits for the previous
}
// Fast: Parallel
await Promise.all([1, 2, 3, 4, 5].map(id => fetchUser(id)));
❌ Forgetting try/catch
// Unhandled rejection crashes Node.js!
async function risky() {
const data = await mightFail(); // No try/catch
}
❌ await in constructor
// You can't use await in constructors!
class MyClass {
constructor() {
this.data = await loadData(); // SyntaxError!
}
}
// ✅ Fix: Use static factory method
class MyClass {
data: any;
private constructor(data) { this.data = data; }
static async create() {
const data = await loadData();
return new MyClass(data);
}
}
const instance = await MyClass.create();
Promise Utilities
// Promise.all() — All must succeed
const [a, b, c] = await Promise.all([p1, p2, p3]);
// If ANY fails → throws immediately
// Promise.allSettled() — Wait for ALL (success or fail)
const results = await Promise.allSettled([p1, p2, p3]);
// Always returns array of { status, value/reason }
// Promise.race() — First to resolve OR reject
const winner = await Promise.race([slowFetch, fastFetch]);
// Returns as soon as one completes
// Promise.any() — First to succeed
const firstSuccess = await Promise.any([
fetch(url1).catch(() => {}),
fetch(url2).catch(() => {}),
]);
// Returns first successful result, ignores failures
Quick Reference
| Pattern | Code |
|---|---|
| Sequential | const a = await p1; const b = await p2; |
| Parallel | const [a, b] = await Promise.all([p1, p2]); |
| Error handling | try { await p } catch (e) {} |
| Timeout | Promise.race([p, timeout(5000)]) |
| Retry | Loop with try/catch and delay |
| Map+parallel | await Promise.all(items.map(fn)) |
What async pattern do you use most? Any I missed?
Follow @armorbreak for more JavaScript content.
Top comments (0)