async-patterns · git:20260614.2f67b30 · 2026-06-14 · sha256 b6bc002843e7c830
async-patterns git:20260614.2f67b30A
Immutable. This exact content is served forever at /api/v1/blob/b6bc002843e7c830.
---
name: async-patterns
description: Async and concurrency pattern advisor for JavaScript/TypeScript, Python, Go, and Rust. Identifies race conditions, missing error handling in promise chains, N+1 async anti-patterns, unbounded parallelism, and missing cancellation. Recommends correct patterns: Promise.all, p-limit, AbortController, AsyncLocalStorage, worker threads. Use when async code has bugs, timeouts, or memory leaks.
---
# Async Patterns
Correct async code is hard. This skill finds the bugs and shows the fix.
## Quick Start
```
/async-patterns — audit current codebase async patterns
/async-patterns --file src/api.ts — analyze single file
/async-patterns --race-conditions — focus on race condition detection
/async-patterns --fix — apply safe pattern upgrades
```
## Common Anti-Patterns and Fixes
### 1. Sequential instead of parallel (N+1 async)
```typescript
// ❌ 100 users × 50ms each = 5,000ms
for (const userId of userIds) {
const user = await db.user.findById(userId)
users.push(user)
}
// ✅ all in parallel = ~50ms
const users = await Promise.all(userIds.map(id => db.user.findById(id)))
// ✅ with concurrency limit (avoid overwhelming DB)
import pLimit from 'p-limit'
const limit = pLimit(10)
const users = await Promise.all(userIds.map(id => limit(() => db.user.findById(id))))
```
### 2. Swallowed promise rejections
```typescript
// ❌ fire and forget — errors silently disappear
sendEmail(user).catch(() => {}) // hiding the error
doSomething() // no await, no catch
// ✅ explicit non-critical handling
sendEmail(user).catch(err => logger.warn('Email failed, non-critical', { err }))
// ✅ or make it explicit you don't care (and document why)
void sendAnalytics(event) // non-critical telemetry, intentional fire-and-forget
```
### 3. Missing AbortController (uncancellable fetches)
```typescript
// ❌ fetch keeps running even if component unmounts / request times out
useEffect(() => {
fetch('/api/data').then(r => r.json()).then(setData)
}, [])
// ✅ cancellable with timeout
useEffect(() => {
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), 5000)
fetch('/api/data', { signal: controller.signal })
.then(r => r.json())
.then(setData)
.catch(err => { if (err.name !== 'AbortError') setError(err) })
.finally(() => clearTimeout(timer))
return () => controller.abort()
}, [])
```
### 4. Race condition in state updates
```typescript
// ❌ if user types fast, older response can overwrite newer one
const [data, setData] = useState(null)
async function search(query: string) {
const result = await fetch(`/api/search?q=${query}`)
setData(await result.json()) // stale response can arrive last
}
// ✅ ignore stale responses with version counter
const requestId = useRef(0)
async function search(query: string) {
const id = ++requestId.current
const result = await fetch(`/api/search?q=${query}`)
if (id === requestId.current) setData(await result.json())
}
```
### 5. Unbound parallelism (memory/connection exhaustion)
```typescript
// ❌ spawns 10,000 concurrent DB connections
await Promise.all(records.map(r => processRecord(r)))
// ✅ bounded concurrency
import pLimit from 'p-limit'
const limit = pLimit(20)
await Promise.all(records.map(r => limit(() => processRecord(r))))
```
### 6. Missing error boundary in Promise.all
```typescript
// ❌ one failure rejects all — you lose partial results
const [users, orders, products] = await Promise.all([
getUsers(),
getOrders(),
getProducts(),
])
// ✅ use Promise.allSettled when partial results are acceptable
const results = await Promise.allSettled([getUsers(), getOrders(), getProducts()])
const [users, orders, products] = results.map(r =>
r.status === 'fulfilled' ? r.value : null
)
```
### 7. Mutex for shared mutable state (Node.js)
```typescript
// ❌ two concurrent requests can read same balance, both deduct, overdraft
async function deductBalance(userId: string, amount: number) {
const user = await db.user.findById(userId)
if (user.balance < amount) throw new Error('Insufficient funds')
await db.user.update(userId, { balance: user.balance - amount })
}
// ✅ database-level atomic update
await db.user.updateMany({
where: { id: userId, balance: { gte: amount } },
data: { balance: { decrement: amount } },
})
// Check affected count — 0 means insufficient funds
```
### 8. Context propagation (AsyncLocalStorage)
```typescript
// Share request context across async calls without prop drilling
import { AsyncLocalStorage } from 'async_hooks'
const requestContext = new AsyncLocalStorage<{ requestId: string; userId: string }>()
// Middleware
app.use((req, res, next) => {
requestContext.run({ requestId: req.id, userId: req.user?.id }, next)
})
// Anywhere in the call chain
function logEvent(event: string) {
const ctx = requestContext.getStore()
logger.info(event, { requestId: ctx?.requestId })
}
```
## Audit Output
```
ASYNC AUDIT — 2026-06-14
==========================
Files analyzed: 34 | Issues: 12
CRITICAL
[C1] src/api/reports.ts:45 — UNBOUNDED PARALLELISM
Promise.all over reportIds (up to 5,000 items)
→ Wrap with pLimit(20)
[C2] src/hooks/useSearch.ts:23 — RACE CONDITION
Stale response can overwrite latest result
→ Add requestId counter or use AbortController
HIGH
[H1] src/workers/emailQueue.ts — SWALLOWED REJECTION
sendEmail().catch(() => {}) — errors lost silently
→ Log and retry or push to dead-letter queue
MEDIUM
[M1] src/api/users.ts:67 — SEQUENTIAL N+1
for...await loop over 50 users
→ Promise.all with pLimit(10)
```