Promise 从原理到实战:状态机、微任务与并发控制全解析

发布时间:2026/10/17 21:36:26

Promise 从原理到实战:状态机、微任务与并发控制全解析 你说你写 JavaScript 写了几年闭包、原型链都没问题一碰到异步就头皮发麻。回调地狱还是小事真正恶心的是那种“明明看着像同步代码结果顺序完全不对”“Promise 明明成功了数据却丢了”“并发一上来接口直接超时白屏”的诡异问题。这篇文章我就把 Promise 从原理到实战彻底拆一遍从状态机、微任务机制、并发控制到 TypeScript 的类型处理、内存泄漏排查该踩的坑我都替你踩过了你直接照着抄作业就行。不管你是在校学生、刚转前端的新手还是已经写了几年业务代码想补理论短板的开发者这篇文章都能给你提供一套完整的 Promise 知识框架和可直接复用的代码方案。1. 异步编程的前世今生为什么非要有 Promise1.1 回调地狱一切问题的起点先回到最原始的场景。你要实现一个简单的流程先拿用户信息再拿用户订单列表最后拿订单详情。用纯回调来写大概是这个样子getUserInfo(userId, (user) { getOrders(user.id, (orders) { getOrderDetail(orders[0].id, (detail) { render(detail); }); }); });三层嵌套还能勉强读但只要你再加上错误处理、并发分支、超时逻辑这段代码就彻底失控了。每一层缩进都在增加认知负担而且错误处理极其棘手——到底在哪个环节出错出错之后要不要继续执行后续步骤回调函数里再抛一个回调你根本不知道错误应该由谁来负责。我当年维护过一个老项目里面的“回调瀑布”最深能到八层。改一个需求要在十几个回调之间传递上下文变量变量名一会儿是data、一会儿是result、一会儿是response肉眼根本判断不了哪个是哪个。那种痛苦用过的人都懂。回调模式的核心问题有三个缺乏标准化的错误处理通道、无法组合和复用、控制流变得极其脆弱。比如你想要“两个请求并行执行等全部完成后一起处理”回调怎么写只能靠一个计数器手动数次数数错了就完蛋。这显然不是工程化的解决方案。1.2 Promise 到底是什么从状态机看懂核心设计Promise 本质上是一个状态机。它只有三种状态pending进行中还没有结果fulfilled成功完成有一个结果值rejected失败有一个失败原因这三种状态之间只能发生两次转换从pending到fulfilled或者从pending到rejected。一旦状态确定就永久锁定不会再变。这个设计的精妙之处在于状态不可逆。回调可以反复调用你不小心在回调里多调用了一次接口数据可能就重复了但 Promise 的状态只能确定一次无论你调用resolve多少次只有第一次生效。用一个生活类比来理解Promise 就好像你去餐厅点了一份外卖。你下单那一刻订单状态是“已下单”pending。之后要么是“已送达”fulfilled要么是“已取消”rejected。订单状态不可能从“已送达”退回“已下单”也不可能既有“已送达”又有“已取消”。而then方法里面传入的回调就相当于你在下单时跟商家说“送到了打个电话通知我”——不管什么时候送到商家只要送达了就会通知你你要是后来才注册的通知只要没送达你也能收到。const promise new Promise((resolve, reject) { // 同步执行 setTimeout(() { resolve(外卖到了); }, 1000); }); promise.then((value) { console.log(value); // 1秒后输出外卖到了 });这里有个很多人忽略的重点new Promise(executor)里的executor函数是同步执行的也就是说resolve或reject是在创建 Promise 的同时就被调用的。但是then里面的回调是异步执行的。为什么要这样设计为了保证Promise的行为一致——无论resolve是被同步调用还是异步调用then里面的回调永远会在当前同步代码执行完毕之后才执行。这一点在下面讲微任务的时候还会再展开。2. Promise 基础核心从 API 到执行机制的完整拆解2.1 创建 Promise 与状态转换的细节创建一个 Promise 的标准方式就是上面写过的new Promise(executor)。但实际开发中我们通常不会直接这样写而是封装成一个函数function fetchUserInfo(userId) { return new Promise((resolve, reject) { const xhr new XMLHttpRequest(); xhr.open(GET, /api/users/${userId}); xhr.onload () { if (xhr.status 200 xhr.status 300) { resolve(JSON.parse(xhr.responseText)); } else { reject(new Error(HTTP ${xhr.status})); } }; xhr.onerror () reject(new Error(网络错误)); xhr.send(); }); }这里面有几个易踩的细节第一executor里如果直接throw一个错误等价于调用reject(error)new Promise((resolve, reject) { throw new Error(boom); }); // 等价于 new Promise((resolve, reject) { reject(new Error(boom)); });第二resolve传入另一个 Promise 时会发生“状态接管”。也就是说A Promise resolve 了一个 B PromiseA 的状态会跟随 B 的状态变化const b new Promise((resolve) setTimeout(() resolve(B结果), 1000)); const a new Promise((resolve) resolve(b)); a.then((value) console.log(value)); // 1 秒后输出B结果第三调用resolve之后再执行executor里剩余的同步代码是合法的但很难排查问题new Promise((resolve) { resolve(done); console.log(这段代码仍然会执行); });所以规范的做法是resolve之后立刻return避免继续执行后面多余的逻辑。2.2 then 链与错误处理为什么 catch 要放在最后then方法接收两个回调一个处理成功一个处理失败。但更常见的用法是只传成功回调然后用catch统一处理错误fetchUserInfo(1) .then((user) { console.log(user); return fetchOrders(user.id); }) .then((orders) { console.log(orders); return fetchOrderDetail(orders[0].id); }) .then((detail) render(detail)) .catch((error) { console.error(链路中任何一步出错都会到这里, error); });这里的关键是then方法本身会返回一个新的 Promise所以可以无限链式调用。而且链路上任意一环抛出的错误都会被后面的catch捕获。这就是 Promise 相对回调最大的优势——错误处理有了统一的出口。但是有一个很经典的坑如果catch没有放在链尾而是放在中间情况就完全不同了fetchUserInfo(1) .then((user) { throw new Error(用户数据有问题); }) .catch((error) { console.error(捕获到错误, error.message); }) .then(() { // 这个 then 会正常执行 console.log(链路继续); });catch返回的也是一个 Promise而且默认状态下它已经“恢复正常”了。所以你 catch 完之后链路还会继续往下走除非你在 catch 里重新抛出错误或者返回一个 rejected 的 Promise。这个行为的实际含义是catch 既能捕获错误也能让链路“复活”。如果你是想“出错就中断”就必须在 catch 里throw或return Promise.reject(...)。另外then里的错误处理回调第二个参数和catch有个重要区别then(onFulfilled, onRejected)里的onRejected只能捕获它前一个Promise 的错误不能捕获同一个then里onFulfilled抛出的错误。而catch放在链尾能捕获它之前所有环节的错误。Promise.resolve(1) .then( (value) { throw new Error(onFulfilled 抛错); }, (error) { // 这个回调不会执行因为它只能处理前一个 Promise 的 rejection console.log(不会打印); } ) .catch((error) { console.log(这个才会捕获到, error.message); });2.3 finally 的副作用与返回值特性finally的意思是“无论成功失败最后都会执行”。它适合做清理工作关闭 loading 状态、释放资源、断开连接等。fetchData() .then((data) render(data)) .catch((error) renderError(error)) .finally(() { hideLoading(); });但finally有一个容易误会的特性它不会改变链路上传递的值。如果finally返回一个值这个值会被忽略如果finally抛出错误则会覆盖前面的结果。Promise.resolve(1) .finally(() { return 999; // 这个返回值被忽略 }) .then((value) { console.log(value); // 输出 1而不是 999 }); Promise.resolve(1) .finally(() { throw new Error(finally 里抛错); }) .catch((error) { console.log(error.message); // 输出finally 里抛错 });所以在finally里写清理逻辑时千万不敢写可能抛错的操作。比如你要关一个连接connection.close()本身可能异步失败你用await this.connection.close()直接写在 finally 里就会覆盖掉业务错误。安全的写法是用try...catch包一层再留给内部消化。3. 深入事件循环Promise 的微任务真相3.1 事件循环与任务队列的完整图谱很多人只知道 Promise 是异步的但说不清楚为什么它比setTimeout更快执行。这就要提到 JS 的事件循环机制。JavaScript 引擎同时只能做一件事。它有一个调用栈Call Stack所有同步代码都在这上面执行。遇到异步操作时不会傻等而是把它交给浏览器或 Node.js 的其他线程去处理等结果回来后再放进任务队列里排队。任务队列其实分成两类宏任务队列MacroTask QueuesetTimeout、setInterval、I/O 事件、UI 渲染等微任务队列MicroTask QueuePromise.then的回调、queueMicrotask、MutationObserver等事件循环的运转规则是同步代码执行完毕后优先清空整个微任务队列微任务队列清空了才从宏任务队列里取一个任务执行。每执行完一个宏任务再回头清空微任务队列。所以微任务的优先级高于宏任务。这就是为什么console.log(脚本开始); setTimeout(() { console.log(setTimeout 回调); }, 0); Promise.resolve().then(() { console.log(Promise.then 回调); }); console.log(脚本结束); // 输出顺序 // 脚本开始 // 脚本结束 // Promise.then 回调 // setTimeout 回调setTimeout的 0 毫秒并不是延迟 0 毫秒执行而是“至少延迟 0 毫秒后放入宏任务队列”。同步代码跑完事件循环先去清空微任务队列Promise 的回调自然排在最前面。这个机制用生活类比来说就是事件循环就像一个业务窗口。宏任务是“窗口排队的人”微任务是“你正在处理的这个客户附加的加急需求”。你在给客户办理业务同步代码时客户突然说“帮我加印一份材料”微任务你肯定先处理完这个加急需求再去喊下一位排队的客户。3.2 async/await 的语法糖本质async/await不是一套新机制它只是 Promise 的语法糖。await关键字等价于Promise.then的展开写法// 写法一async/await async function getOrderInfo(userId) { const user await fetchUserInfo(userId); const orders await fetchOrders(user.id); return orders; } // 写法二等价的手写 Promise 链 function getOrderInfo(userId) { return fetchUserInfo(userId).then((user) { return fetchOrders(user.id); }); }关键在于await右边的表达式会被包装成 Promise而await后面的代码会被放入微任务队列。所以async function test() { console.log(进入 async 函数); const result await 1; console.log(await 之后, result); } test(); console.log(同步代码继续执行); // 输出顺序 // 进入 async 函数 // 同步代码继续执行 // await 之后 1哪怕await右边的值是一个同步值比如上文的1后面的代码依然要等待当前同步代码执行完才会运行。很多人写async函数时以为await前面的代码会一口气执行完其实第一个await之前的代码是同步的第一个await后面的代码就全部变成了异步。这一点在多调用并发时特别容易踩坑。3.3 一段魔鬼代码彻底搞懂 Promise 执行顺序把前面说的所有机制放在一起看一段几乎每年面试都会出现的代码async function async1() { console.log(async1 start); await async2(); console.log(async1 end); } async function async2() { console.log(async2); } console.log(script start); setTimeout(() { console.log(setTimeout); }, 0); async1(); Promise.resolve().then(() { console.log(promise1); }); console.log(script end);一步步推演执行同步代码输出script start遇到setTimeout回调被放入宏任务队列调用async1()同步执行到第一个await之前输出async1 start执行await async2()所以进入async2()输出async2await后面的代码console.log(async1 end)不会立即执行而是排入微任务队列回到调用处继续执行Promise.resolve().then(...)promise1回调也排入微任务队列。注意此时微任务队列里有两个任务async1 end和promise1继续执行同步代码输出script end同步代码执行完毕开始清空微任务队列。按照入队顺序先执行async1 end再执行promise1微任务队列清空从宏任务队列取一个任务执行setTimeout回调输出setTimeout所以最终输出顺序是script start async1 start async2 script end async1 end promise1 setTimeout这里最容易出错的地方在于第 6 步await后面的微任务和Promise.then的微任务哪个先入队答案是await后面代码的更先入队。因为await会更早地触发微任务注册。从这套推演里你能直观感受到微任务的注册时机决定了执行顺序而不是你代码写的先后顺序。调试异步代码时如果顺序不对先检查自己的微任务入队顺序再去排查别的。4. 并发场景完全指南all / allSettled / race / any4.1 Promise.all全有或全无一个失败全员失败Promise.all是非常常用的并发调度器参数是一个 Promise 数组准确说是可迭代对象它会等所有 Promise 全部成功后才返回结果数组只要有一个失败整个 Promise 就进入 rejected 状态并且采用第一个失败的原因。const [userInfo, userOrders, userCart] await Promise.all([ fetchUserInfo(userId), fetchOrders(userId), fetchCart(userId), ]);用途非常明确多个互相独立的请求一起发等它们全部回来后统一渲染页面。相比一个接一个的串行等待Promise.all可以把总耗时从“所有请求耗时之和”压缩到“最慢那个请求的耗时”。但Promise.all有一个隐蔽的缺陷快速失败fail-fast。假设你同时发起 10 个请求第 2 个失败了Promise.all立即进入 rejected 状态后面 8 个请求的结果全部被丢弃——但它们发出的 HTTP 请求并不会被取消依然会占着网络资源。这在一些场景下会浪费带宽和计算资源。另外结果数组的顺序必须和你传入的 Promise 数组一一对应。即使某个请求完成得很快它在结果数组里的位置也保持不变。const [slow, fast] await Promise.all([ slowRequest(), // 耗时 2 秒 fastRequest(), // 耗时 0.3 秒 ]); // 结果数组第一个元素是 slowRequest 的结果虽然它最后完成如果业务上“部分失败也能接受”你就得换allSettled。4.2 allSettled / race / any不同场景的场景选型对比Promise.allSettled会等待所有 Promise 全部结束不管成功还是失败并返回每个 Promise 的完整状态信息const results await Promise.allSettled([ fetchUserInfo(userId), fetchBanner(), fetchRecommend(), ]); for (const result of results) { if (result.status fulfilled) { console.log(成功数据, result.value); } else { console.log(失败原因, result.reason); } }典型场景是首页这种“有几个模块挂了不影响页面主框架”的请求组合。你希望尽量展示能展示的东西而不是因为一个推荐位接口报错就把整个首页白屏掉。Promise.race的含义是“只看第一个改变状态的结果”const timeout new Promise((_, reject) { setTimeout(() reject(new Error(请求超时)), 5000); }); const data await Promise.race([fetchData(), timeout]);给请求加超时是最常见的race用法。但必须注意race已经结束了不代表其他 Promise 不会继续执行它们依然在后台运行只是结果被丢弃。如果你在fetchData里打开了某个资源超时之后也需要自行回收。Promise.any相对冷门它的语义是“只要有一个成功就返回第一个成功的值”全部失败才进入 rejectedconst firstAvailable await Promise.any([ fetchFromSourceA(), fetchFromSourceB(), fetchFromSourceC(), ]);适合“从多个镜像源选一个最快的可用源”这种场景。四个并发 API 的对比如下API成功条件失败条件典型场景all全部成功只要一个失败依赖所有数据的页面渲染allSettled所有 Promise 结束永不失败容错型请求、上报类任务race第一个结束的状态第一个结束的状态超时控制、最快来源any只要有一个成功全部失败多源容灾、CDN 切换4.3 手写并发限制器控制请求数量的实战不加控制的Promise.all有一个实际问题如果你有一个 500 个元素的数组要并发发请求一口气全发出去服务端可能直接被压崩浏览器也会因为连接数限制而排队。所以生产环境的经验是——永远要做并发限制。经典的并发限制器实现思路其实不复杂维护一个“正在执行”的任务池用计数器和队列来调度。async function mapLimit(items, limit, handler) { const results []; const executing new Set(); for (const item of items) { const promise handler(item).then((result) { results.push(result); executing.delete(promise); }); executing.add(promise); if (executing.size limit) { await Promise.race(executing); } } await Promise.allSettled(executing); return results; }这段代码的思路是不断往executing集合里添加新的 Promise同时利用Promise.race等在池子里的任意一个任务结束任务结束之后把自己从集合里删除主循环再放一个新任务进来从而把同时执行的 Promise 数量限制在limit以内。使用示例const userIds Array.from({ length: 100 }, (_, i) i 1); const userInfos await mapLimit(userIds, 5, (id) fetchUserInfo(id));我再补充一个更贴近实战的场景批量拉取商品价格。一次拉 5 个拉完再拉下一批页面给出进度条。你用计时器观察一下串行大约耗时 20 秒假设每批 2 秒限制 5 并发大约 4 秒差异非常明显。并发数不是越大越好。最常见的经验值是 3 到 6。大了容易触发服务端限流小了自己等得难受。遇到第三方 API还要去看对方的限流文档别拍脑袋填 10。5. TypeScript 中的 Promise类型安全与泛型陷阱5.1 Promise 泛型推导别让 TS 把你的类型变成 anyPromise 在 TypeScript 中的完整类型是PromiseTT就是成功后的值类型。定义一个 Promise 时最好显式标明function fetchUserInfo(userId: number): PromiseUserInfo { return fetch(/api/users/${userId}) .then((response) response.json()) .then((data) data as UserInfo); }不建议写成Promiseany。一旦某个接口被标成any之后所有字段错误都是运行时才炸TypeScript 的保护就完全失效了。更好的做法是用泛型为整个fetch封装一层async function getJSONT(url: string): PromiseT { const response await fetch(url); if (!response.ok) { throw new Error(HTTP ${response.status} ${response.statusText}); } return response.json() as PromiseT; } const user await getJSONUserInfo(/api/users/1);这里有个 TypeScript 的隐蔽坑response.json()返回的类型本身是Promiseany。所以你要用as PromiseT来断言而不是直接赋给PromiseT变量。直接写const user: UserInfo await response.json()编译能过是因为any能赋值给任何类型——这是 TS 对any的特殊放行并不代表运行时安全。真正的运行时校验你还需要用 zod 之类的库类型断言只是编译期约束。5.2 async 函数返回值与错误处理类型一个async函数无论你 return 什么它在类型层面永远返回PromiseTasync function getUsername(): Promisestring { return 张三; // 实际返回 Promisestring } const username await getUsername(); // 类型是 string这带来一个常见问题你忘了加await就把一个Promisestring当成了string用。TS 在绝大多数情况下会报错但不报错的情况也有——如果目标类型恰好是Promisestring那就会“正常”地把一个 Promise 塞进去等到运行时才出问题。所以建议在自己的代码规范里严格要求调用 async 函数必须显式await除非你确实要拿到 Promise 本身做组合。错误处理里最烦人的是catch的error类型。在 TypeScript 4.0 之后catch的变量类型默认是unknown不是anytry { await getJSONUserInfo(/api/users/1); } catch (error) { // error 类型是 unknown你不能直接 error.message if (error instanceof Error) { console.error(error.message); } else { console.error(非 Error 类型的异常, error); } }生产环境里我建议封装一个错误处理工具函数function toErrorMessage(error: unknown): string { if (error instanceof Error) return error.message; if (typeof error string) return error; if (error typeof error object message in error) { return String((error as { message: string }).message); } return 未知错误; }5.3 容易被类型问题坑到的三个场景第一个坑数组map配合async函数你以为得到的是普通数组实际得到的是 Promise 数组。const ids [1, 2, 3]; const users ids.map(async (id) getJSONUserInfo(/api/users/${id})); // users 的类型是 PromiseUserInfo[] const realUsers await Promise.all(users); // 这才是 UserInfo[]第二个坑嵌套泛型推导失败。Promise.all的返回类型按理说是[A, B]这样的元组但如果你传入的是动态展开的数组TS 会退化成(A | B)[]你拿不到对应的每一项类型。解决方式是使用as const或者显式声明类型const [a, b] await Promise.all([ fetchA(), fetchB(), ] as const);第三个坑finally里不能await一个可能 reject 的 Promise前面已经讲过。在 TS 类型层面它通常不会报错但运行时会覆盖原始错误。所以牢记finally只做清理不做结果处理。6. 实战排查Promise 的常见陷阱与调试技巧6.1 内存泄漏悬空 Promise 与未处理的拒绝很多前端开发者对内存泄漏没什么概念觉得 JS 有 GC 就万事大吉。但 Promise 相关的内存泄漏真实存在最常见的形态是一个永远停在 pending 状态的 Promise被全局变量引用着永远得不到释放。比如你用Promise.race做超时控制超时之后只是 reject 了但那个被丢弃的 Promise 还在后台跑。如果它内部的闭包捕获了一大块数据又没有被回收运行一段时间后内存就上去了。更隐蔽的是事件监听器挂在那不解除const button document.getElementById(submit); button.addEventListener(click, async () { const data await fetchData(); // 每次点击都会创建新的 Promise 和新的闭包 });如果组件销毁时没有移除监听器历史 Promise 及其闭包会一直驻留。在单页应用里页面的切换会让这个问题迅速暴露。排查手段打开浏览器的 Performance 面板录制一段时间观察内存曲线是否只升不降。如果是用 Memory 面板抓一次堆快照过滤Promise对象看有多少。6.2 未处理的 Promise RejectionNode.js 不会放过你Node.js 从 15 版开始默认就会在进程退出时检查有没有未处理的 Promise rejection有就直接报错退出。浏览器里未处理的 rejection 也会触发unhandledrejection事件。什么情况下会产生未处理的 rejection很简单你创建了一个 rejected 的 Promise但是全程没有给它挂catch。async function main() { const promise fetchData(); // 这个 Promise 如果 reject 了 // 这里没有 await也没有 .catch() // 如果 fetchData 失败了就会触发 unhandledrejection }这种问题非常难排查因为它不一定在触发时就报错。你在浏览器里调试时等很久才在控制台看到 “Uncaught (in promise)”。预防手段有三个层级全局兜底在入口处监听unhandledrejection事件把错误统一记录和上报。开发规范所有 Promise 要么await要么显示挂catch不允许裸奔。类型约束TS 编译器可以配合no-floating-promises规则强制检查没有处理的 Promise。我自己习惯在团队代码检查规则里开启typescript-eslint/no-floating-promises这条规则能提前拦下一大批潜在运行时错误。6.3 调试技巧实录从 Chrome 到 Node.js调试 Promise 有个尴尬断点打在then回调里看不到这个 Promise 是谁创建的。Chrome DevTools 的解决办法是开启 “Async” 调试模式它会保留异步调用栈。实际操作Chrome 开发者工具 - Sources 面板 - 右上角勾选Async。这样当你断在 Promise 回调里时右侧 Call Stack 里会显示完整的异步调用链能直接看到是哪个函数触发的这个 Promise。Node.js 环境下的利器是--trace-rejections参数node --trace-rejections app.js它会在 Promise 被 reject 时打印完整的堆栈哪怕你这个 rejection 后来被捕获了它也会打印只是注释不同。这对定位“到底谁 reject 了这个 Promise”极其有用。还有一个我实际开发中经常用的小技巧给Promise增加一个可读的标识方便在堆栈里区分不同的操作。function makePromiseWithName(name, task) { const promise task(); promise.name name; return promise; }严格来说 Promise 对象上没有name属性但你可以在调试时利用命名函数来代替const fetches { user: () getJSON(/api/user), orders: () getJSON(/api/orders), }; // 在堆栈里能看到调用者函数名比匿名箭头函数清晰得多追踪内存和未处理 rejection 时还建议打开node --inspect配合 Chrome DevTools 的 Memory 面板能抓到具体的未释放 Promise 的引用路径。6.4 从 then 链到 async/await 的实战重构对照很多老项目里还是成串的.then链可读性尚可但一旦分支复杂就非常吃力。比如这种结构fetchUserInfo(userId) .then((user) { if (!user.isVip) { return fetchNormalBanner().then((banner) ({ user, banner })); } return fetchVipBanner(user.level).then((banner) ({ user, banner })); }) .then(({ user, banner }) { render(user, banner); }) .catch((error) handleError(error));重构为 async/await 就直观很多try { const user await fetchUserInfo(userId); const banner user.isVip ? await fetchVipBanner(user.level) : await fetchNormalBanner(); render(user, banner); } catch (error) { handleError(error); }这不是什么高深技巧但实战中很多人在重构时忽略了try/catch的作用域。注意上面的写法里handlerError能捕获到fetchUserInfo、条件判断、两个fetch、render任何一步的错误。你要是把try包得太窄漏掉某些分支的错误处理反而比 then 链更糟糕。所以我的经验是async/await 重构 then 链时宁可把 try 的范围扩大一些也不要为“精确捕获”而把 try 拆得七零八落。JS 的错误处理本身就不精细你把范围收窄反而制造二次坑。最后再说一个我最近实际踩过的坑。某次线上接口偶发超时我以为是服务端问题排查了很久才发现是前端Promise.all里掺入了一个会卡 5 秒才 reject 的脚本加载逻辑导致整个catch看起来“延迟触发”。后来用Promise.race给所有并发任务都包了超时兜底问题才彻底解决。所以你真要稳定所有网络请求都要有超时兜底不要指望服务端一定会在合理时间内返回。Promise 本身不会超时超时必须你自己写这也是我反复在各处强调的一点。用Promise.race封装一个通用超时工具function withTimeoutT(promise: PromiseT, timeoutMs: number): PromiseT { const timeout new Promisenever((_, reject) { setTimeout(() reject(new Error(请求超时${timeoutMs}ms)), timeoutMs); }); return Promise.race([promise, timeout]); } // 使用 const data await withTimeout(fetchData(), 5000);这个工具几乎可以无痛接入所有现有异步代码也是我在所有项目里必须维护的公共方法之一。你在接入时注意把超时时间配置成可传参方便针对不同接口做差异化设置。
延伸阅读

更多相关文章

2026/10/17 21:36:26

Kafka接入AI实战:从数据管道到异步推理的架构设计

1. 为什么AI项目越做越绕不开Kafka前阵子帮一个做实时风控的团队排查消费堆积,整整一下午盯着Kafka的lag数据,从几万涨到几十万,消费者一直处于"拉消息-处理-再拉"的状态,但就是追不上。最后发现问题不在Kafka本身&…

2026/10/17 21:36:26

从工具调用到具身执行:AI Agent走向物理世界的关键技术路线

从去年到今年,我聊过不少做AI-Agent团队的技术同学,大家普遍有一个共同的感受:纯数字世界里的Agent,已经卷到头了。不是不能做,而是做得再多,价值链条始终断在“物理世界”这一环。查天气、订机票、操作浏览…

2026/10/17 21:36:26

从浅数据到深数据:私域变现的四步实操路径

私域这个词被讲得太多了。多到很多团队一听到就头疼,因为拉群、发券、朋友圈轰炸全都试过,结果除了把自己搞累、把用户搞烦,并没有沉淀出真正能持续变现的东西。我接触了不少做私域的项目之后,发现一个很明显的分水岭:…

2026/10/17 22:46:39

CytoNCA实战:生物网络中心性分析与关键节点筛选

简介:CytoNCA是一款基于Cytoscape平台的生物网络集中性分析与评估插件,面向从事蛋白质相互作用网络、基因调控网络及蛋白质结构网络研究的人员,帮助其从复杂拓扑结构中识别关键节点与重要蛋白质。资源包内含1个PDF文件,大小约1.37…

2026/10/17 22:46:39

大数据分析驱动的农业气象灾害预警实战指南

简介:数据驱动的智能决策正深刻改变传统气象服务模式。在农业领域,气象灾害预警依赖对海量观测数据的实时处理与时空关联分析。通过引入大数据处理框架和机器学习模型,能够从站点观测、格点再分析等多源数据中提取灾害演变的规律,…

2026/10/17 22:46:39

Kepserver 连接 MySQL 实战:ODBC 驱动配置与数据落地避坑指南

简介:这份PDF资料面向工业自动化与数据采集方向的工程师及运维人员,聚焦Kepserver与MySQL数据库的对接场景,解决设备数据落库与可视化管理的实际问题。内容涵盖MySQL数据库安装、Navicat可视化管理工具的安装与破解、32位ODBC驱动配置&#x…

2026/10/17 22:46:39

Netty NIO核心组件详解:Buffer、Channel、Selector原理与实战

1. 从一次线上故障说起:为什么我要回头啃Netty的NIO底座前两年我接手过一个即时通讯服务的性能优化项目,单机压测的时候QPS死活上不去,CPU占用率却高得离谱。当时第一反应是业务代码写得有问题,排查了一圈发现线程池配置、锁竞争、…

2026/10/17 22:46:39

大模型提问指南:解题式与启发式,如何划清人与工具的边界

跟大语言模型打了一年多交道,我越来越觉得,“怎么提问”这件事,本质上是一场关于边界的划分。同样面对一个LLM,有人把它当成二十四小时在线的执行器,有人把它当成帮自己整理思路的思考搭子。这就是标题里说的两种思路&…

2026/10/17 22:41:38

UML建模思维训练:从画图规范到设计意图的深度转化

简介:本资源是面向软件工程专业学生及系统分析设计初学者的UML核心考点复习资料,聚焦类图、交互图、关系建模与设计原则等高频难点,助力期末备考与软考基础夯实。压缩包为单个132KB的Word文档(.doc),内容结…

2026/10/16 5:33:43

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/16 4:03:48

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/17 14:20:58

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/17 0:04:32

PlanarSLAM:面向曼哈顿世界的点线面联合SLAM系统

简介:本资源是面向机器人视觉与SLAM研究者的C开源RGB-D SLAM系统实现,专为结构化室内场景(如办公室、走廊)设计,通过融合点、线、平面三类几何特征并引入“曼哈顿世界”假设,显著提升位姿跟踪鲁棒性与稠密地…

2026/10/17 0:04:32

TDH Workflow调度实战:从DAG依赖驱动到任务编排与故障排查

1. 从一个调度脚本说起:TDH Workflow到底在解决什么问题第一次接触TDH的Workflow,是在一个数据清洗项目里。当时团队每天要跑几十个ETL任务,任务之间有严格的先后依赖——A表清洗完才能跑B表的关联,B表跑完才能触发C表的聚合。最开…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑