6 种防止生产环境中部分失败的异步 JavaScript 模式

发布: (2026年3月28日 GMT+8 05:00)
5 分钟阅读
原文: Dev.to

Source: Dev.to

大多数异步代码在工作流进行到一半时都能正常运行,但一旦某一步失败,就会导致重复收费、数据缺失或静默损坏。
这些模式可防止生产环境中的部分失败。

1. 用补偿步骤替代顺序 Await

顺序代码看起来简洁,但在部分失败时会出现问题。

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 }
}

如果发货失败,付款已经完成——没有回滚。

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')
  }
}

你显式地定义回滚逻辑——这正是实际系统的做法。

2. 提前启动独立的 Promise

大多数开发者不小心把独立的工作串行化了。

之前

async function loadData(userId: string) {
  const user = await fetchUser(userId)
  const analytics = await fetchAnalytics(userId)
  return { user, analytics }
}

总耗时 = 两个调用的时间之和。

之后

async function loadData(userId: string) {
  const analyticsPromise = fetchAnalytics(userId)

  const user = await fetchUser(userId)
  const analytics = await analyticsPromise

  return { user, analytics }
}

在不改变逻辑的前提下,你可以减少延迟。这个模式经常出现在高级面试中。

3. 使用 Promise.allSettled 保护多调用流程

仪表盘不应因某个 API 宕机而整体失败。

之前

const [users, orders, stats] = await Promise.all([
  fetchUsers(),
  fetchOrders(),
  fetchStats()
])

一次失败会导致所有请求都失败。

之后

const results = await Promise.allSettled([
  fetchUsers(),
  fetchOrders(),
  fetchStats()
])

const [users, orders, stats] = results.map(r =>
  r.status === 'fulfilled' ? r.value : null
)

现在可以渲染部分数据。失败变为可观察的,而不是灾难性的。

4. 仅对瞬态错误进行重试并使用退避

重试所有操作比不重试更糟。

之前

await fetch('/api/payment')

一次网络波动就会中断流程。

之后

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'))

仅在有意义时才进行重试,避免对 API 进行猛烈请求。这在处理如Node.js memory leak debugging scenarios中描述的不可靠外部系统时尤为关键,因为错误的重试会放大系统压力。

5. 使用 AbortController 取消过时请求

竞争条件是最常见的生产环境 bug 之一。

Before

useEffect(() => {
  fetch(`/api/search?q=${query}`)
    .then(r => r.json())
    .then(setResults)
}, [query])

旧的响应会覆盖新的响应。

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])

现在只有最新的请求会生效,不会出现过时的 UI。

6. 限制并发而不是淹没 API

一次性发送 100 个请求会导致被限流或封禁。

Before

await Promise.all(ids.map(id => fetchItem(id)))

无限并发。

After

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

  async function worker() {
    while (i  () => fetchItem(id))
await limit(tasks, 5)

你可以控制吞吐量,使系统在负载下保持稳定。

结束语

今天就挑选一个现有的异步流程,添加补偿或取消机制。这单独就能消除一整类生产环境的错误。

如果你的代码只假设成功路径,那么它已经出现问题了。

0 浏览
Back to Blog

相关文章

阅读更多 »

MCP的七宗罪:运营罪

操作罪:懒惰与愤怒 这些罪行属于此类别,因为它们决定了实时 MCP 系统在压力下的行为方式:它是否真实地失败……

网络怀旧

概述 我一直对互联网的快速演变感到着迷。从90年代那种杂乱、色彩斑斓的网站,到今天的简洁、极简设计——它……