Promise异常处理:forEach与for...of的差异及问题咨询
你实现了以下Node.js(TypeScript)方法,父函数triggerPromotion调用子函数checkEntityForPromotion:
async triggerPromotion(fileNames: string[]): Promise<string> { var entityList = await this.listEntity(); fileNames.forEach(async fileName => { var matchingEntity = entityList.entities.filter(rec => rec.ref.replaceAll("\\", "/").includes(fileName))[0] if (matchingEntity != null) { await this.checkEntityForPromotion(matchingEntity) } }) return Promise.reject( new BizError({ message: BizErrorTips.PROMOTION_FAILED, description: 'detail description in here.', }), ); }
async checkEntityForPromotion(entity: Entity): Promise<Boolean> { var versionToBePromoted = entity.version var existingVersion = await ValidateReqUtil.getAzureStreamingVersion(entity.name) // 如果待升级版本与低环境版本不匹配,则停止后续操作 if (versionToBePromoted != existingVersion) { return Promise.reject(`Entity version to be promoted ${versionToBePromoted} is not the same as existing entity version ${existingVersion}`) } return Promise.resolve(true) }
当checkEntityForPromotion抛出异常时,使用forEach循环会触发未处理Promise拒绝错误:
UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "Entity version to be promoted 1.0.2 is not the same as existing entity version 1.0.1".
但替换成for...of循环后,异常能被正确捕获。
核心差异原理
forEach的行为本质:
forEach是数组迭代方法,设计初衷是处理同步任务。当传入async回调函数时,forEach只会立即执行回调,不会等待回调中的await完成,也不会收集回调返回的Promise对象。每个async回调返回的Promise都是独立存在的,一旦某个Promise被拒绝,没有任何代码会捕获这个拒绝(除非回调内部手动添加.catch()),最终导致未处理的Promise拒绝错误。for...of的行为本质:
for...of是ES6引入的迭代语句,运行在async函数内部时,会配合await等待每个异步任务完成。在for...of循环中使用await this.checkEntityForPromotion(...)时,await会暂停当前函数执行,直到该Promise完成。如果Promise被拒绝,这个拒绝会向上冒泡到async函数的上下文,最终能被上层错误处理逻辑捕获。
补充:forEach处理异步任务的正确方式
如果一定要用forEach处理异步逻辑,需要手动收集所有回调返回的Promise,用Promise.all()统一等待并处理错误:
async triggerPromotion(fileNames: string[]): Promise<string> { const entityList = await this.listEntity(); // 收集所有异步任务的Promise const asyncTasks = fileNames.map(async fileName => { const matchingEntity = entityList.entities.filter(rec => rec.ref.replaceAll("\\", "/").includes(fileName))[0]; if (matchingEntity) { await this.checkEntityForPromotion(matchingEntity); } }); try { await Promise.all(asyncTasks); } catch (error) { throw new BizError({ message: BizErrorTips.PROMOTION_FAILED, description: error.toString(), }); } return "Promotion completed successfully"; }
内容的提问来源于stack exchange,提问作者Prabal Rakshit

