The Problem with Unstructured Async Code
JavaScript async code has a scope problem. You fire off promises and hope they complete—or fail—cleanly. When something goes wrong mid-flight, cleanup is your responsibility.
// Classic problem: partial failure
async function loadDashboard(userId: string) {
const [user, orders, analytics] = await Promise.all([
getUser(userId),
getOrders(userId), // This throws after 2 seconds
getAnalytics(userId), // This is still running!
]);
// getAnalytics never gets cancelled
}
When getOrders rejects, Promise.all rejects—but getAnalytics keeps running in the background, consuming resources, potentially writing stale data.
Promise.allSettled: Handle All Results
async function loadDashboard(userId: string) {
const results = await Promise.allSettled([
getUser(userId),
getOrders(userId),
getAnalytics(userId),
]);
const [userResult, ordersResult, analyticsResult] = results;
// Handle each independently
const user = userResult.status === 'fulfilled' ? userResult.value : null;
const orders = ordersResult.status === 'fulfilled' ? ordersResult.value : [];
if (analyticsResult.status === 'rejected') {
console.error('Analytics failed:', analyticsResult.reason);
}
return { user, orders, analytics: analyticsResult.status === 'fulfilled' ? analyticsResult.value : null };
}
AbortController: Actual Cancellation
async function fetchWithTimeout<T>(url: string, timeoutMs: number): Promise<T> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, { signal: controller.signal });
return response.json();
} finally {
clearTimeout(timeoutId);
}
}
// Cancel when component unmounts (React)
function useUserData(userId: string) {
const [data, setData] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetch(`/api/users/${userId}`, { signal: controller.signal })
.then(r => r.json())
.then(setData)
.catch(err => {
if (err.name !== 'AbortError') throw err;
// AbortError is expected on cleanup, ignore it
});
return () => controller.abort(); // cleanup
}, [userId]);
return data;
}
Promise.race: First Wins
// Timeout pattern
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
const timeout = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms)
);
return Promise.race([promise, timeout]);
}
// Fastest cache or network
async function getWithFallback(key: string) {
return Promise.race([
redis.get(key).then(v => JSON.parse(v!)), // cache
db.slowQuery(key), // database
]);
}
Promise.any: First Success
// Try multiple mirrors, use whichever responds first
async function fetchFromCDN(path: string) {
return Promise.any([
fetch(`https://cdn1.example.com${path}`),
fetch(`https://cdn2.example.com${path}`),
fetch(`https://cdn3.example.com${path}`),
]);
// Rejects only if ALL fail (AggregateError)
}
Controlled Concurrency
Running 1000 tasks in parallel overwhelms databases and APIs:
async function processInBatches<T, R>(
items: T[],
processor: (item: T) => Promise<R>,
concurrency: number
): Promise<R[]> {
const results: R[] = [];
for (let i = 0; i < items.length; i += concurrency) {
const batch = items.slice(i, i + concurrency);
const batchResults = await Promise.all(batch.map(processor));
results.push(...batchResults);
}
return results;
}
// Process 1000 users, 10 at a time
const results = await processInBatches(users, processUser, 10);
Or use p-limit for a semaphore pattern:
import pLimit from 'p-limit';
const limit = pLimit(10); // max 10 concurrent
const results = await Promise.all(
users.map(user => limit(() => processUser(user)))
);
// All 1000 tasks are queued, but only 10 run at once
Async Iteration
async function* generateUsers(): AsyncGenerator<User> {
let page = 1;
while (true) {
const users = await db.users.findMany({ skip: (page - 1) * 100, take: 100 });
if (users.length === 0) return;
yield* users;
page++;
}
}
// Process without loading all into memory
for await (const user of generateUsers()) {
await sendEmail(user.email);
}
The right concurrency primitive depends on your use case:
-
Promise.all— all must succeed -
Promise.allSettled— handle each result individually -
Promise.race— first finishes wins -
Promise.any— first succeeds wins -
AbortController— actually cancel in-flight requests -
p-limit— controlled parallelism
Async patterns, concurrency utilities, and error handling built in: Whoff Agents AI SaaS Starter Kit.
Top comments (0)