DashboardData(userId: string) {
const profile = await fetchUserProfile(userId);
const notifications = await fetchNotifications(userId);
const metrics = await fetchDashboardMetrics(userId);
return { profile, notifications, metrics };
}
// β
Solution: Parallel execution
async function loadDashboardDataOptimized(userId: string) {
const [profile, notifications, metrics] = await Promise.all([
fetchUserProfile(userId),
fetchNotifications(userId),
fetchDashboardMetrics(userId)
]);
return { profile, notifications, metrics };
}
**Rationale:** `Promise.all` accepts an array of promises. The promises are created immediately when the array is evaluated, triggering the underlying operations. The `await` only pauses until the collective resolution. This reduces total wait time to the duration of the slowest request.
#### 2. Safe Iteration with Controlled Concurrency
`forEach` should never be used with async callbacks. Instead, choose between sequential processing (`for...of`) or parallel processing (`map` + `Promise.all`). For large datasets, implement a concurrency limiter to prevent resource exhaustion.
```typescript
// β
Sequential processing (ordered, safe)
async function processOrderBatchSequential(orders: Order[]) {
const results: ProcessResult[] = [];
for (const order of orders) {
const result = await validateAndProcess(order);
results.push(result);
}
return results;
}
// β
Parallel processing with concurrency limit
async function processOrderBatchParallel(
orders: Order[],
concurrencyLimit: number = 5
): Promise<ProcessResult[]> {
const results: ProcessResult[] = [];
const executing = new Set<Promise<void>>();
for (const order of orders) {
const p = validateAndProcess(order).then(result => {
results.push(result);
executing.delete(p);
});
executing.add(p);
if (executing.size >= concurrencyLimit) {
await Promise.race(executing);
}
}
await Promise.all(executing);
return results;
}
Rationale: The concurrency limiter maintains a set of active promises. When the limit is reached, it awaits the fastest resolving promise before adding more work. This prevents overwhelming downstream services or hitting file descriptor limits.
3. Resilient Batch Operations
Use Promise.allSettled when the failure of one item should not abort the entire batch. This is critical for operations like bulk imports, cache warming, or analytics reporting.
interface BatchResult<T> {
id: string;
status: 'fulfilled' | 'rejected';
value?: T;
reason?: Error;
}
async function syncInventoryBatch(items: InventoryItem[]): Promise<BatchResult<string>[]> {
const promises = items.map(item =>
syncItemToWarehouse(item)
.then(value => ({ id: item.id, status: 'fulfilled' as const, value }))
.catch(reason => ({ id: item.id, status: 'rejected' as const, reason }))
);
return Promise.allSettled(promises).then(results =>
results.map((res, index) => ({
id: items[index].id,
status: res.status,
value: res.status === 'fulfilled' ? res.value : undefined,
reason: res.status === 'rejected' ? res.reason : undefined
}))
);
}
Rationale: Promise.allSettled waits for all promises to complete regardless of outcome. Wrapping individual promises with .catch ensures the rejection is captured in the result object rather than causing the allSettled promise to reject.
Pitfall Guide
1. The forEach Illusion
- Explanation:
Array.prototype.forEach does not await async callbacks. It invokes the callback and immediately moves to the next element, ignoring the returned promise. The loop finishes before any async work completes, and errors inside the callback are unhandled.
- Fix: Replace
forEach with for...of for sequential logic or map combined with Promise.all for parallel logic.
2. Latency Multiplication
- Explanation: Chaining
await on independent calls forces the runtime to wait for each to finish before starting the next. If three API calls take 100ms each, sequential execution takes 300ms, while parallel takes ~100ms.
- Fix: Identify independent calls and group them in
Promise.all. Use dependency graphs to determine which calls can run concurrently.
3. Unbounded Concurrency Spikes
- Explanation: Using
Promise.all on a large array (e.g., 10,000 items) creates thousands of simultaneous connections. This can exhaust memory, hit OS file descriptor limits, or trigger rate limits on external APIs, causing cascading failures.
- Fix: Implement chunking or a concurrency limiter. Process items in batches of a safe size (e.g., 10-50) depending on the resource constraints.
4. Silent Promise Leaks
- Explanation: Forgetting
await on a function call returns a Promise object instead of the resolved value. The code continues execution, often leading to type errors or logic bugs later. No exception is thrown at the call site.
- Fix: Enable TypeScript strict mode and ESLint rules like
@typescript-eslint/await-thenable and require-await. Use linters to catch missing awaits during development.
5. Catastrophic Failure in Promise.all
- Explanation:
Promise.all rejects immediately if any promise rejects. In a batch operation, a single transient error can abort the processing of thousands of successful items, requiring a full retry.
- Fix: Use
Promise.allSettled for non-critical batches. For critical operations, implement retry logic around individual items rather than relying on the aggregate promise.
6. Awaiting Non-Promises in Map
- Explanation: Developers sometimes write
await array.map(async item => ...) expecting resolved values. This returns an array of Promises, not values. The await resolves the array promise immediately, leaving an array of unresolved promises.
- Fix: Use
Promise.all(array.map(...)) to resolve the array of promises, or use for...of if sequential resolution is intended.
7. Race Conditions in Shared State
- Explanation: Parallel async operations modifying shared state (e.g., a global counter or cache) without synchronization can lead to data corruption. JavaScript is single-threaded, but
await yields control, allowing other code to run between operations.
- Fix: Avoid shared mutable state in parallel flows. Use immutable updates or ensure operations are idempotent. For critical sections, serialize access or use atomic patterns.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Dependent Data Fetch | Sequential await | Data B requires Data A | Low latency overhead |
| Independent API Calls | Promise.all | Minimizes latency | High risk if any call fails |
| Bulk Import (Non-Critical) | Promise.allSettled | Tolerates partial failures | Slightly higher complexity |
| Large Dataset Processing | Chunked/Concurrency Limit | Prevents rate limits/OOM | Higher code complexity |
| Ordered Side Effects | for...of + await | Preserves execution order | O(N) latency |
Configuration Template
ESLint Configuration for Async Safety
Add these rules to your .eslintrc.js to catch common async mistakes automatically:
module.exports = {
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
rules: {
// Prevents missing await on promises
'@typescript-eslint/await-thenable': 'error',
// Ensures async functions actually await something
'@typescript-eslint/require-await': 'warn',
// Flags void promises that might be missing await
'@typescript-eslint/no-floating-promises': 'error',
// Enforces consistent await usage
'require-await': 'off', // Disable base rule if using TS plugin
},
};
Concurrency Limiter Utility
// utils/concurrency.ts
export async function runWithConcurrency<T, R>(
items: T[],
worker: (item: T) => Promise<R>,
limit: number
): Promise<R[]> {
const results: R[] = [];
const executing: Promise<void>[] = [];
for (const item of items) {
const p = worker(item).then(result => {
results.push(result);
});
executing.push(p);
if (executing.length >= limit) {
await Promise.race(executing);
executing.splice(executing.indexOf(await Promise.race(executing)), 1);
}
}
await Promise.all(executing);
return results;
}
Quick Start Guide
- Install Linting: Add
@typescript-eslint/eslint-plugin and configure no-floating-promises and await-thenable rules. Run the linter to identify immediate issues.
- Refactor Top Bottlenecks: Locate functions with multiple sequential awaits. If calls are independent, wrap them in
Promise.all and measure latency improvement.
- Fix Iteration Bugs: Replace all
forEach loops containing await with for...of or map patterns. Verify behavior with unit tests.
- ** Harden Batch Ops:** For any operation processing lists of items, switch to
Promise.allSettled if partial success is acceptable. Add logging for rejected items.
- Load Test: Run performance tests on refactored endpoints. Verify that concurrency limits prevent resource exhaustion under load and that error handling behaves as expected.