- 后端
【免费下载链接】bluebird
:bird: :zap: Bluebird is a full featured promise library with unmatched performance.
导读
.timeout()是 Bluebird 中用于给任意 Promise 追加“最晚完成时间”的核心方法:只要目标 Promise 在指定毫秒数内没有进入 fulfilled 或 rejected 状态,返回的新 Promise 就会被立即拒绝,拒绝原因是内置的Promise.TimeoutError(或你自定义的错误)。本文以官方 API 文档 docs/docs/api/timeout.md 为骨架,结合 src/timers.js 的底层实现与 test/mocha/timers.js 的测试用例,完整讲解两种调用签名、自定义消息与错误对象、超时后的取消行为、计时器清理机制以及配套的TimeoutError类型,读完即可在自己的项目中安全地给文件读取、网络请求等不确定操作加上超时保护。
.timeout()的两种调用签名
根据官方 API 文档,.timeout()支持两种签名,区别只在于第二个参数的类型:
.timeout( int ms, [String message="operation timed out"] ) -> Promise.timeout( int ms, [Error error] ) -> Promise- 第一个参数
ms是超时毫秒数(整数),必填; - 第二个参数可选:
- 传入String时,超时后拒绝原因是
new TimeoutError(message),即自定义超时错误消息; - 传入Error实例时,超时后直接以该错误对象作为拒绝原因;
- 不传时,默认错误消息为
"operation timed out"(该字符串定义在 src/constants.js 的TIMEOUT_ERROR常量中)。
- 传入String时,超时后拒绝原因是
返回的新 Promise 会沿用原 Promise 的fulfillment 值或 rejection 原因:如果原 Promise 在ms毫秒内正常完成,那么.timeout()返回的 Promise 也以同样的值 fulfill 或同样的原因 reject,行为与直接使用原 Promise 完全一致;只有超时这一种情况下它才会以自己的方式拒绝。这一点在 src/timers.js 中体现为successClear与failureClear两个透传处理器——它们在透传值/原因的同时,只负责清理底层定时器句柄。
基础用法:给异步操作加上超时保护
文档给出的经典示例是为文件读取设置 100ms 超时:
var Promise = require("bluebird"); var fs = Promise.promisifyAll(require('fs')); fs.readFileAsync("huge-file.txt").timeout(100).then(function(fileContents) { }).catch(Promise.TimeoutError, function(e) { console.log("could not read file within 100ms"); });这里需要几个 Bluebird 前置知识,全部在仓库中有据可查:
Promise.promisifyAll是 promisify.js 提供的批量 Promise 化 API,会给fs的所有方法生成以Async结尾的 Promise 版本(readFileAsync即由此而来),相关细节见 docs/docs/api/promise.promisifyall.md;Promise.TimeoutError是挂载在Promise构造函数上的内置错误类型,由 src/promise.js 的Promise.TimeoutError = errors.TimeoutError赋值,其定义位于 src/errors.js;.catch(Promise.TimeoutError, function(e){...})是 Bluebird 的类型化错误捕获(catch_filter.js),只有拒绝原因是TimeoutError实例时才会进入该分支,普通读取错误(如文件不存在)会继续向下传播。
测试 test/mocha/timers.js 从三个角度验证了基础行为(见describe("timeout", ...)用例):
- 快速完成不做任何事:
Promise.delay(1).timeout(200)正常 fulfill,不会误伤; - 快速拒绝原样传递:
.timeout(200)后捕获到的错误error === goodError,即原 Promise 的拒绝原因被原样透传,没有替换成超时错误; - 确实超时才拒绝:
Promise.delay(1).timeout(10)因 10ms 小于 1ms 之后完成的延迟,被Promise.TimeoutError拒绝。
自定义超时错误:字符串消息与 Error 对象
文档指出:“When using the first signature, you may specify a custom error message with themessageparameter.” 即第一种签名可用字符串自定义消息,第二种签名则可直接传入一个 Error 对象。两者分别有对应的测试佐证:
// 字符串消息 Promise.delay(1) .timeout(10, "custom") .caught(Promise.TimeoutError, function(e){ assert(/custom/i.test(e.message)); // e.message 包含 "custom" }); // Error 对象 var err = Error("Testing Errors"); Promise.delay(1) .timeout(10, err) .caught(function(e){ assert(e === err); // 拒绝原因就是同一个对象 });从源码 src/timers.js 的afterTimeout函数可以看清底层分支逻辑:
- 若
message是字符串,则err = new TimeoutError(message); - 若
message是Error实例,则直接复用该对象作为拒绝原因(这正是测试中断言e === err成立的原因); - 若两者都不是(比如传了
null或未传),则回退为new TimeoutError(TIMEOUT_ERROR),即默认消息"operation timed out"。
此外afterTimeout还做了两件容易被忽略的事:通过util.markAsOriginatingFromRejection(err)把该错误标记为“源于拒绝”,并通过promise._attachExtraTrace(err)将错误附加到 Promise 的追踪链上——这两步与 Bluebird 的长堆栈追踪(long stack traces)机制配合,能帮助你在开启调试时定位“超时发生前”的完整调用来源。
超时后的取消行为(cancellation 模式)
.timeout()与 Bluebird 的取消机制(docs/docs/api/cancellation.md)紧密相关。在 src/timers.js 的实现中:
Promise.prototype.timeout = function (ms, message) { ms = +ms; var ret, parent; var handleWrapper = new HandleWrapper(setTimeout(function timeoutTimeout() { if (ret.isPending()) { afterTimeout(ret, message, parent); } }, ms)); if (debug.cancellation()) { parent = this.then(); ret = parent._then(successClear, failureClear, undefined, handleWrapper, undefined); ret._setOnCancel(handleWrapper); } else { ret = this._then(successClear, failureClear, undefined, handleWrapper, undefined); } return ret; };要点如下:
- 定时器回调
timeoutTimeout中先用ret.isPending()判断返回的 Promise 是否仍处于 pending 状态,只有仍 pending 才会触发超时拒绝——这是“先完成者胜”语义的保证; - 当通过
Promise.config({cancellation: true})开启取消支持(见 docs/docs/api/promise.config.md)后,parent = this.then()会创建一个内部父 Promise 快照,超时触发时afterTimeout(ret, message, parent)会调用parent.cancel()取消原 Promise 链,从而让底层操作(如仍在进行中的 I/O 回调)尽早释放; HandleWrapper包装了setTimeout返回的句柄,并通过_setOnCancel(handleWrapper)把句柄与返回 Promise 的取消操作绑定,一旦返回的 Promise 被取消,_resultCancelled会立即clearTimeout清理定时器,避免定时器泄漏。
测试 test/mocha/timers.js 对取消行为做了正反两面的验证:
- 有且仅有一个消费者时:
p.timeout(11)超时后,原 Promise 链p被取消,其.then(...)回调didNotExecute保持为true(父 promise 确实被取消了); - 存在多个消费者时:如果同一个原 Promise 还被其他分支派生过(
var derived = p.then(...)),则超时取消不会波及共享的其他消费者,derived仍会正常执行——这是取消传播的“引用计数”保护语义。
计时器句柄的可靠清理
.timeout()返回的 Promise 一旦完成(无论 fulfill 还是 reject),底层setTimeout都必须被清除,否则在 Node.js 等环境中会造成句柄悬挂。这一机制由successClear/failureClear两个处理器承担(src/timers.js):
function successClear(value) { clearTimeout(this.handle); return value; } function failureClear(reason) { clearTimeout(this.handle); throw reason; }this是注入的handleWrapper,this.handle即setTimeout返回的句柄;- 二者在透传结果/原因的同时执行
clearTimeout,保证超时定时器不会在 Promise 提前完成的情况下继续悬挂。
test/mocha/timers.js 中的 “timer handle clearouts” 测试组专门验证了这一点:测试通过替换全局clearTimeout来捕获调用参数,分别断言快速 fulfill(Promise.delay(1).timeout(10000))和快速 reject(setTimeout(reject, 10)后接.timeout(10000))两种场景下,clearTimeout都以正确的句柄类型被调用,从侧面证实了定时器在完成路径上必然被清理。
配套类型:TimeoutError
.timeout()默认使用的拒绝原因是 Bluebird 内置的TimeoutError,其构造签名见 docs/docs/api/timeouterror.md:
new TimeoutError(String message) -> TimeoutErrorTimeoutError表示“某个操作已超时”,专门作为.timeout()的默认取消/拒绝原因使用。在源码层面:
- 它由 src/errors.js 的
subError("TimeoutError", "timeout error")工厂函数创建,继承自原生Error,未传 message 时的默认消息为"timeout error"; - 它与其他内置错误类型(
CancellationError、OperationalError、AggregateError、Warning)一起被冻结并注册到Error[BLUEBIRD_ERRORS](src/errors.js),这一设计确保了同一进程中多份 Bluebird 拷贝共享相同的错误类型,跨库实例捕获时类型判断依然成立; - 你通过
Promise.TimeoutError(src/promise.js)即可访问该类型,用于.catch(Promise.TimeoutError, handler)的类型化捕获,或用于instanceof判断。
常见问题与最佳实践
1. 超时后原 Promise 还在执行吗?默认(未开启 cancellation)情况下,.timeout()只是让“结果”超时作废,原 Promise 底层的异步操作仍会继续运行,只是其结果不再被消费。若希望超时后尽早中止底层操作,请先调用Promise.config({cancellation: true})(docs/docs/api/promise.config.md),并注意多消费者场景下取消不会波及共享分支(见上文测试)。
2. 如何区分“超时失败”与“业务失败”?推荐使用类型化捕获,让两种失败各走各的分支:
op().timeout(5000).then(function(value) { // 5 秒内完成 }).catch(Promise.TimeoutError, function(e) { // 5 秒未完成,e.message 为自定义消息或 "operation timed out" }).catch(function(e) { // 其他业务错误 });3.ms参数的类型处理实现中ms = +ms(src/timers.js)做了隐式数值转换,因此传入字符串形式的数字(如"100")也会被当作毫秒数处理;但请始终传入正整数以避免语义混乱。
4. 超时与Promise.delay组合使用.timeout()与Promise.delay(docs/docs/api/promise.delay.md、src/timers.js)同属 Timers 家族(docs/docs/api/timers.md),常搭配使用:Promise.delay(ms, value)是“延迟后再完成”,.timeout(ms)是“超过时限即失败”,二者一缓一急,共同构成对异步时序的完整控制能力。
小结
.timeout()以极小的 API 面提供了完整的 Promise 超时语义:两种签名覆盖“自定义消息”与“自定义错误对象”两种需求;底层通过setTimeout+isPending()检查保证先完成者胜,通过successClear/failureClear保证计时器句柄在任何完成路径上都被清理;配合cancellation: true时还能在超时后主动取消原 Promise 链。其默认错误类型TimeoutError可经由Promise.TimeoutError做类型化捕获。理解 src/timers.js 的实现与 test/mocha/timers.js 的测试,能让你在文件 I/O、网络请求、数据库访问等场景中放心地把超时控制交给 Bluebird。
- 后端
【免费下载链接】bluebird
:bird: :zap: Bluebird is a full featured promise library with unmatched performance.
相关推荐
Soul Signature 技术规格解析:RuView 基于 RVF 图结构的七通道多模态被动电磁生物特征签名
Soul Signature 技术规格解析:RuView 基于 RVF 图结构的七通道多模态被动电磁生物特征签名 导读 :Soul Signature 是 Ru
后端Bluebird 定时器 API 深度指南:`.delay` 延迟与 `.timeout` 超时机制全解析
Bluebird 定时器 API 深度指南: .delay 延迟与 .timeout 超时机制全解析 导读 在 Bluebird 中,"Timers" 指一组用
后端Hono 如何用 timeout 中间件为耗时请求设置超时并返回自定义异常?
Hono 如何用 timeout 中间件为耗时请求设置超时并返回自定义异常? 如果某个 Hono 请求处理函数需要等待较慢的下游(数据库、第三方 API 等),
后端Web框架
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考