6 Async JavaScript Patterns That Prevent Partial Failures in Production
Source: Dev.to
Most async code works fine until one step fails halfway through a workflow, leading to double charges, missing data, or silent corruption.
These patterns prevent partial failures in production.
1. Replace Sequential Awaits With Compensated Steps
Sequential code looks clean but breaks on partial failure.
Before
async function processOrder(orderId: string) {
const order = await fetchOrder(orderId)
const payment = await chargeCustomer(order.customerId, order.total)
const shipment = await createShipment(order.items, order.address)
return { order, payment, shipment }
}If shipment fails, payment is already done—no rollback.
After
async function processOrder(orderId: string) {
const order = await fetchOrder(orderId)
let payment
try {
payment = await chargeCustomer(order.customerId, order.total)
} catch {
throw new Error('PAYMENT_FAILED')
}
try {
const shipment = await createShipment(order.items, order.address)
return { order, payment, shipment }
} catch {
await refundPayment(payment.id).catch(() => {
logger.fatal('REFUND FAILED', { orderId })
})
throw new Error('SHIPMENT_FAILED')
}
}You explicitly define rollback logic—what real systems do.
2. Start Independent Promises Early
Most developers accidentally serialize independent work.
Before
async function loadData(userId: string) {
const user = await fetchUser(userId)
const analytics = await fetchAnalytics(userId)
return { user, analytics }
}Total time = sum of both calls.
After
async function loadData(userId: string) {
const analyticsPromise = fetchAnalytics(userId)
const user = await fetchUser(userId)
const analytics = await analyticsPromise
return { user, analytics }
}You save latency without changing logic. This pattern shows up in senior interviews.
3. Guard Multi-Call Flows With Promise.allSettled
Dashboards should not fail entirely because one API is down.
Before
const [users, orders, stats] = await Promise.all([
fetchUsers(),
fetchOrders(),
fetchStats()
])One failure kills everything.
After
const results = await Promise.allSettled([
fetchUsers(),
fetchOrders(),
fetchStats()
])
const [users, orders, stats] = results.map(r =>
r.status === 'fulfilled' ? r.value : null
)Now partial data renders. Failures become observable, not catastrophic.
4. Retry Only Transient Errors With Backoff
Retrying everything is worse than not retrying.
Before
await fetch('/api/payment')One network hiccup breaks the flow.
After
async function retry(fn, attempts = 3) {
for (let i = 0; i setTimeout(r, 2 ** i * 100))
}
}
throw new Error('FAILED_AFTER_RETRIES')
}
await retry(() => fetch('/api/payment'))You retry only when it makes sense, avoiding hammering APIs. This becomes critical when dealing with flaky external systems like described in the Node.js memory leak debugging scenarios, where retries amplify system pressure if done wrong.
5. Cancel Stale Requests With AbortController
Race conditions are one of the most common production bugs.
Before
useEffect(() => {
fetch(`/api/search?q=${query}`)
.then(r => r.json())
.then(setResults)
}, [query])Old responses overwrite new ones.
After
useEffect(() => {
const controller = new AbortController()
fetch(`/api/search?q=${query}`, {
signal: controller.signal
})
.then(r => r.json())
.then(setResults)
.catch(err => {
if (err.name !== 'AbortError') throw err
})
return () => controller.abort()
}, [query])Now only the latest request wins. No stale UI.
6. Limit Concurrency Instead of Flooding APIs
Firing 100 requests at once will get you rate‑limited or banned.
Before
await Promise.all(ids.map(id => fetchItem(id)))Unbounded concurrency.
After
async function limit(tasks, concurrency) {
const results = []
let i = 0
async function worker() {
while (i () => fetchItem(id))
await limit(tasks, 5)You control throughput, keeping systems stable under load.
Closing
Take one of your existing async flows and add compensation or cancellation today. That alone removes an entire class of production bugs.
If your code assumes the happy path, it is already broken.