DEV Community

JSGuruJobs
JSGuruJobs

Posted on

6 Async JavaScript Patterns That Prevent Partial Failures in Production

Most async code works fine until one step fails halfway through a workflow. Then you get double charges, missing data, or silent corruption.
These patterns fix that.


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 }
}
Enter fullscreen mode Exit fullscreen mode

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')
  }
}
Enter fullscreen mode Exit fullscreen mode

You explicitly define rollback logic. This is 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 }
}
Enter fullscreen mode Exit fullscreen mode

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 }
}
Enter fullscreen mode Exit fullscreen mode

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()
])
Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

One network hiccup breaks the flow.

After

async function retry(fn, attempts = 3) {
  for (let i = 0; i < attempts; i++) {
    try {
      return await fn()
    } catch (e) {
      if (e.status && e.status < 500) throw e
      await new Promise(r => setTimeout(r, 2 ** i * 100))
    }
  }
  throw new Error('FAILED_AFTER_RETRIES')
}

await retry(() => fetch('/api/payment'))
Enter fullscreen mode Exit fullscreen mode

You retry only when it makes sense. This avoids 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])
Enter fullscreen mode Exit fullscreen mode

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])
Enter fullscreen mode Exit fullscreen mode

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)))
Enter fullscreen mode Exit fullscreen mode

Unbounded concurrency.

After

async function limit(tasks, concurrency) {
  const results = []
  let i = 0

  async function worker() {
    while (i < tasks.length) {
      const current = i++
      results[current] = await tasks[current]()
    }
  }

  await Promise.all(
    Array.from({ length: concurrency }, worker)
  )

  return results
}

const tasks = ids.map(id => () => fetchItem(id))
await limit(tasks, 5)
Enter fullscreen mode Exit fullscreen mode

You control throughput. Systems stay 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.

Top comments (0)