← 返回题目列表

如何手写 Promise 重试函数?需要处理哪些边界?

中等 第 21 / 27 题 更新于 2026/07/29
手写代码Promise重试异步

简化版

Promise 重试函数用于把一个可能失败的异步任务包装成“失败后再试几次”。核心是:传入返回 Promise 的函数 fn,失败后判断剩余次数,必要时等待一段时间再递归或循环调用。边界包括:只重试失败、不重试成功;次数语义要清楚;支持延迟和退避;保留最后一次错误;不要对已经创建好的 Promise 重试。

详细版

重试函数的重点不是语法,而是“每次重试都要重新执行任务函数”。如果传入的是 Promise 实例,它已经开始执行,失败后无法重新发起。

说明
入参fn 必须是函数,调用后返回 Promise
次数retries 表示失败后最多再试几次
延迟可固定延迟,也可指数退避
错误次数耗尽后抛出最后一次错误
function sleep(ms: number) {
  return new Promise(resolve => setTimeout(resolve, ms))
}

async function retry<T>(fn: () => Promise<T>, retries = 3, delay = 0): Promise<T> {
  try {
    return await fn()
  } catch (err) {
    if (retries <= 0) throw err
    if (delay > 0) await sleep(delay)
    return retry(fn, retries - 1, delay)
  }
}

面试里要特别说清楚:重试的是“任务函数”,不是已经执行过的 Promise。

完整版教学

一、题目考察点

这道题表面是 Promise,实际考察异步控制流、错误传递、递归或循环、次数边界和工程思维。

很多候选人能写出 happy path,但容易忽略“Promise 一旦创建就开始执行”,所以传参方式很关键。

二、为什么不能传 Promise 实例

错误写法是:

retry(fetch('/api'), 3)

fetch('/api') 已经发出请求了。失败后你只能重新等待同一个失败结果,不能重新请求。

正确写法是:

retry(() => fetch('/api'), 3)

每次调用 fn() 都会创建新的异步任务。

三、递归版本怎么写

递归版本最直观:执行任务,成功直接返回,失败判断是否还有次数,有就等待后再次调用。

async function retry<T>(fn: () => Promise<T>, retries: number): Promise<T> {
  try {
    return await fn()
  } catch (error) {
    if (retries === 0) throw error
    return retry(fn, retries - 1)
  }
}

这里 retries 表示“失败后还可以重试几次”。如果 retries = 3,最多执行 4 次。

四、循环版本怎么写

循环版本更容易控制退避、日志和中断。

async function retryLoop<T>(fn: () => Promise<T>, retries = 3, delay = 0): Promise<T> {
  let lastError: unknown
  for (let i = 0; i <= retries; i++) {
    try {
      return await fn()
    } catch (error) {
      lastError = error
      if (i < retries && delay > 0) await sleep(delay)
    }
  }
  throw lastError
}

它的优势是不会形成很深的 Promise 链,扩展也方便。

五、指数退避和抖动

真实请求重试不能所有客户端同时固定 1 秒重试,否则服务恢复时会被同时打爆。常见做法是指数退避和随机抖动。

策略示例作用
固定延迟1000ms简单可控
指数退避500、1000、2000ms减轻下游压力
随机抖动延迟加随机值避免请求同时涌入

六、哪些错误不该重试

不是所有失败都适合重试。网络超时、503、限流可以重试;参数错误、鉴权失败、业务校验失败通常不该重试。

可以增加 shouldRetry 参数:

type RetryOptions = {
  retries: number
  shouldRetry?: (error: unknown) => boolean
}

七、常见误区与追问

  • 误区:传入 Promise 就能重试。 Promise 已经执行,必须传返回 Promise 的函数。
  • 误区:所有错误都应该重试。 业务错误和鉴权错误通常重试也不会成功。
  • 误区:重试次数就是总执行次数。 要先约定清楚是“额外重试次数”还是“总尝试次数”。
  • 追问:如何支持指数退避? 每次失败后按尝试次数计算 delay,例如 base * 2 ** attempt
  • 追问:如何保留错误? 次数耗尽时抛出最后一次捕获到的错误。
  • 追问:如何支持取消? 可以传入 AbortSignal,每次重试前检查是否已取消。

八、加强记忆

手写重试记住四件事:任务函数、失败再试、次数耗尽抛最后错误、延迟退避防雪崩。答题时把“不能传 Promise 实例”点出来,基本就稳了一半。