Async Patterns in Node.js: Promises, async/await, and Concurrency
Promise Fundamentals
// Creating Promises
const delay = (ms: number): Promise<void> =>
new Promise(resolve => setTimeout(resolve, ms));
const fetchUser = (id: string): Promise<User> =>
new Promise((resolve, reject) => {
db.find(id, (err, user) => {
if (err) reject(err);
else if (!user) reject(new Error('Not found'));
else resolve(user);
});
});
Parallel vs Sequential Execution
// Sequential (slow - each waits for previous)
const user1 = await fetchUser('1');
const user2 = await fetchUser('2');
const user3 = await fetchUser('3');
// Parallel (fast - all run concurrently)
const [user1, user2, user3] = await Promise.all([
fetchUser('1'),
fetchUser('2'),
fetchUser('3'),
]);
// First to resolve wins
const firstAvailable = await Promise.race([
fetchFromPrimary(id),
delay(500).then(() => fetchFromFallback(id)),
]);
// All settle (don't fail on individual errors)
const results = await Promise.allSettled([
fetchUser('1'),
fetchUser('invalid'),
fetchUser('3'),
]);
results.forEach(result => {
if (result.status === 'fulfilled') console.log(result.value);
else console.error(result.reason);
});
Concurrency Control
// Process array with limited concurrency (p-limit pattern)
async function mapWithConcurrency<T, R>(
items: T[],
fn: (item: T) => Promise<R>,
concurrency: number
): Promise<R[]> {
const results: R[] = [];
const queue = [...items];
const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
while (queue.length > 0) {
const item = queue.shift()!;
const result = await fn(item);
results.push(result);
}
});
await Promise.all(workers);
return results;
}
// Usage: process 100 users but only 5 at a time
const processed = await mapWithConcurrency(userIds, processUser, 5);
Error Handling Patterns
// Wrapping with Result type
async function safeAsync<T>(
fn: () => Promise<T>
): Promise<{ data: T; error: null } | { data: null; error: Error }> {
try {
return { data: await fn(), error: null };
} catch (err) {
return { data: null, error: err as Error };
}
}
const { data: user, error } = await safeAsync(() => fetchUser(id));
if (error) {
console.error('Failed to fetch user:', error.message);
return;
}
// TypeScript knows user is not null here
// Retry logic
async function withRetry<T>(
fn: () => Promise<T>,
maxRetries = 3,
baseDelay = 1000
): Promise<T> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (err) {
if (attempt === maxRetries) throw err;
await delay(baseDelay * Math.pow(2, attempt));
}
}
throw new Error('unreachable');
}
Async Iteration
// Process database results as stream
async function* streamUsers(filter: UserFilter) {
let page = 0;
const pageSize = 100;
while (true) {
const users = await userRepo.findAll({ ...filter, page, pageSize });
if (users.length === 0) break;
yield* users;
page++;
}
}
for await (const user of streamUsers({ role: 'admin' })) {
await processUser(user);
}
AbortController for Cancellation
async function fetchWithTimeout<T>(
fn: (signal: AbortSignal) => Promise<T>,
timeoutMs: number
): Promise<T> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
return await fn(controller.signal);
} finally {
clearTimeout(timeoutId);
}
}
// Usage
const user = await fetchWithTimeout(
(signal) => fetch('/api/user', { signal }).then(r => r.json()),
5000
);
Common Anti-Patterns
// Bad: Unnecessary await in map creates sequential execution
const users = await Promise.all(ids.map(async id => {
return await fetchUser(id); // 'await' inside async map is fine but redundant
}));
// Bad: Forgetting to await
async function updateAllUsers(updates: UserUpdate[]): Promise<void> {
updates.forEach(async update => { // Bug! forEach doesn't await Promises
await processUpdate(update);
});
}
// Good: Use for...of or Promise.all
async function updateAllUsers(updates: UserUpdate[]): Promise<void> {
for (const update of updates) {
await processUpdate(update);
}
// or: await Promise.all(updates.map(processUpdate));
}
Understanding the Node.js event loop and async patterns is fundamental to writing performant server code.