6 Async JavaScript Patterns That Prevent Partial Failures in Production

Published: (March 27, 2026 at 05:00 PM EDT)
3 min read
Source: Dev.to

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.

0 views
Back to Blog

Related posts

Read more »