前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
首页程序员面试题库Promise.race 超时
JavaScript异步与 Promise

怎么用 Promise.race 给异步操作加超时?超时后原来的 Promise 还会跑吗?

把原任务和一个「到点就 reject」的 Promise 一起传给 Promise.race,谁先敲定就用谁的结果。超时后原 Promise 不会停,它会继续执行到完成,只是结果没人要了;想真正停下请求,要配合 AbortController。

前端进阶之旅 · 一题精讲更新于 2026.09.29
JavaScript#异步与 Promise#异步编程
先看核心答案读代码示例
理解线索

超时封装里的三个角色

  1. 原任务真正干活的 Promise,超时后仍在后台运行
  2. 超时 PromisesetTimeout 到点调用 reject,只负责喊超时
  3. Promise.race只认第一个敲定的结果,后面的一律忽略

超时之后原任务如果失败,也不会触发 unhandledrejection,因为 Promise.race 已经给它挂了处理函数。

核心回答

先记住这个答案

Promise.race 接收一组 Promise,返回的 Promise 跟随最先敲定的那一个,不管它是成功还是失败。所以只要再造一个「N 毫秒后 reject」的 Promise,和原任务一起传进去,原任务先完成就拿到数据,定时器先到就得到超时错误。超时之后,原 Promise 不会被取消,它会继续执行,完成后的结果被 race 忽略。所以还要在结束时清掉定时器;如果希望请求真的停下,要用 AbortController 去中断它。

  • Promise.race 取最先敲定的结果,成功失败都算
  • 超时只是不再等,原任务照样执行完
  • 用 finally 清掉定时器,避免多余的定时器
  • 真正取消请求要把 signal 传给 fetch

用 Promise.race 写一个超时封装

下面的原任务要 200 毫秒,超时时间设成 100 毫秒。

超时了,原任务还在跑JavaScript
function withTimeout(promise, ms) {
  let timer
  const timeout = new Promise((_, reject) => {
    timer = setTimeout(() => reject(new Error('超时')), ms)
  })
  // 无论谁先完成,都把定时器清掉
  return Promise.race([promise, timeout]).finally(() => clearTimeout(timer))
}

const task = new Promise((r) => setTimeout(() => { console.log('原任务仍然跑完了'); r('数据') }, 200))

withTimeout(task, 100).then(
  (v) => console.log('成功', v),
  (e) => console.log('失败', e.message)
)
task.then((v) => console.log('原 Promise 结果', v))
查看输出与解释
失败 超时
原任务仍然跑完了
原 Promise 结果 数据

100 毫秒时超时 Promise 先 reject,于是 race 返回的 Promise 就被拒绝了。200 毫秒时原任务照样执行,打印日志并兑现为「数据」。直接 then 它的地方能拿到结果,只有 race 不再理它。 finally 里清定时器是为了另一种情况:原任务 10 毫秒就成功了,定时器却还挂着,到点再 reject 一次。这次 reject 虽然会被忽略,但定时器白白占着。在 Node.js 里,没清掉的定时器还会让进程晚退出。

让原任务真的停下来

Promise 本身没有取消能力,race 只是换了一个人不再等。如果原任务是网络请求,超时后浏览器还会继续接收响应,占着连接。要真正中断,得让原任务自己支持取消。

超时时顺便中断 fetchJavaScript
async function fetchWithTimeout(url, ms) {
  const controller = new AbortController()
  const timer = setTimeout(() => controller.abort(), ms) // 到点直接中断请求
  try {
    const res = await fetch(url, { signal: controller.signal })
    return await res.json()
  } finally {
    clearTimeout(timer)
  }
}
// 超时后 fetch 以 AbortError 拒绝,Network 面板里请求状态是 canceled

这里不需要 race 了,abort() 会让 fetch 自己以 AbortError 拒绝。现在浏览器还提供了 AbortSignal.timeout(ms),一行就能生成一个到点自动中断的 signal,超时时的错误名是 TimeoutError。

自己写的异步任务怎么支持取消
让函数接收一个 signal 参数,在每个耗时步骤之前检查 signal.aborted,或者监听它的 abort 事件,收到就停止后续工作并 reject。这样外面的超时逻辑只管调用 abort(),任务自己负责收尾。
回答前,多想一步

容易答错的地方

以为超时后原任务就不执行了
race 只决定返回哪个结果,不会影响传进去的 Promise。原任务里的请求、写库、打日志都会照常发生。如果原任务是提交表单,超时提示「失败」之后数据其实可能已经写进去了,重试前要考虑幂等。
用 Promise.race 只能判断成功先到
race 对成功和失败一视同仁。原任务在 50 毫秒时失败了,race 就立刻以这个错误拒绝,不会再等超时。这正是超时封装想要的行为,但有人会误以为它会「等到有一个成功为止」,那是 Promise.any 的语义。
忘了区分超时错误和业务错误
catch 里拿到的可能是超时,也可能是原任务自己的错误。给超时错误一个固定的名字或错误类型,调用方才能给出不同的提示,比如超时提示重试,参数错误提示修改输入。
试着用自己的话回答

面试官还会怎么问?

Promise.race 传空数组会怎样?

返回的 Promise 永远是 pending,因为没有任何一项能敲定它。封装超时函数时一般不会遇到,但写通用的 race 工具时要注意。

超时后想自动重试,怎么写?

在 catch 里判断是超时错误,就再调用一次 withTimeout,并带上剩余重试次数,次数用完再抛出。每次重试都要重新创建原任务,因为旧的 Promise 已经敲定,不会重新执行。

Promise.race 和 Promise.any 有什么区别?

race 取第一个敲定的,不管成功失败;any 取第一个成功的,只有全部失败时才以 AggregateError 拒绝。做超时用 race,做「多个 CDN 谁先成功用谁」用 any。

从一道题,走向一组知识

把知识连起来

异步与 Promise

什么时候会触发 unhandledrejection?怎么避免未处理的 Promise 拒绝?

超时后原任务再失败会不会报未处理拒绝,要结合这题的触发条件来判断。

异步与 Promise

递归处理层级不确定的异步任务,怎么避免爆栈和 Promise 链越来越长?

超时之后往往要重试,递归重试的写法要注意 Promise 链不要无限增长。

参考资料

  • JavaScript:异步与 Promise参考资料

示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。

本题目录
  1. 先记住这个答案
  2. 用 Promise.race 写一个超时封装
  3. 让原任务真的停下来
  4. 容易答错的地方
  5. 面试官还会怎么问
  6. 把知识连起来
读懂,再试着讲出来

先看核心答案,再读代码。最后展开追问,检查自己有没有遗漏边界。

试着回答追问
浏览全部面试题理解原理,也关注真实的使用场景。回到顶部 ↑