本文深入探讨了promise.catch未能捕获错误的常见原因,指出问题可能源于被调函数未正确拒绝promise。在此基础上,文章详细阐述了简单重试机制的局限性,例如引发速率限制和雪崩效应,并提出设计健壮重试策略的重要性。通过提供一个包含指数退避和promise链式调用的优化实现,旨在指导开发者构建更可靠、高效的异步操作重试逻辑。
在使用Promise进行异步操作时,Promise.catch() 方法是用于捕获Promise链中任何拒绝状态的错误。然而,有时开发者会发现即使控制台输出了错误,catch 块却没有被执行。这通常是因为导致错误的函数(例如示例中的 fn)并没有返回一个处于拒绝状态的Promise。
当一个异步函数(如 fetch 或其他自定义函数)在内部发生错误时,它必须显式地返回一个被拒绝的Promise,Promise.catch() 才能捕获到这个错误。如果 fn 函数在内部抛出异常但没有将其封装在一个被拒绝的Promise中,或者它返回了一个成功状态的Promise,那么外部的 catch 块将无法捕获到这个错误。因此,调试此类问题时,首要任务是检查 fn 函数的实现,确保其在错误发生时正确地拒绝Promise。
在网络请求或不稳定服务调用中,重试机制是提高系统健壮性的常见手段。然而,一个设计不当的重试函数可能弊大于利。例如,如果一个请求因为持久性错误(如错误的API密钥、无效的URL或服务器内部错误)而失败,不加间隔地快速连续重试会导致一系列负面影响:
因此,一个健壮的重试系统必须避免“快速失败,快速重试”的简单模式。
为了解决上述问题,生产级别的重试系统通常会采用退避算法 (Backoff Algorithm),即在每次重试之间引入一个逐渐增长的延迟。这种策略有几个关键优势:
常见的退避策略包括线性退避和指数退避。指数退避通常更受欢迎,因为它能更快地拉开重试间隔,尤其是在面对持续性问题时。
以下是一个优化后的 retry 函数实现,它结合了Promise链式调用和指数退避策略,以构建更健壮的异步操作重试机制。
/** * 创建一个延迟Promise * @param t 延迟时间(毫秒) * @returns 一个在指定时间后解决的Promise */ function delay(t: number): Promise{ return new Promise(resolve => setTimeout(resolve, t)); } // 最小重试间隔时间 const kMinRetryTime = 100; // 100毫秒 // 每次重试额外增加的时间 const kPerRetryAdditionalTime = 500; // 500毫秒 /** * 计算退避延迟时间 * @param retries 当前重试次数 * @returns 计算出的延迟时间(毫秒) */ function calcBackoff(retries: number): number { // 第一次重试(retries=1)延迟 kMinRetryTime // 之后每次重试增加 kPerRetryAdditionalTime return Math.max(kMinRetryTime, (retries - 1) * kPerRetryAdditionalTime); } /** * 带有指数退避的重试函数 * @param fn 要重试的异步函数 * @param params 传递给fn的参数 * @param times 最大重试次数 * @returns 原始fn成功解决的Promise,或在达到最大重试次数后抛出原始错误 */ export function retry(fn: Function, params: any, times = 1e9 + 7): Promise { let retries = 0; // 记录当前重试次数 /** * 内部尝试执行函数并处理重试逻辑 * @returns Promise */ function attempt(): Promise { // 执行原始函数 return fn(params).catch((err: Error) => { // 捕获到错误,增加重试次数 ++retries; console.error(`Attempt ${retries} failed:`, err); // 打印错误信息 // 检查是否还有重试次数 if (retries <= times) { // 如果还有重试次数,计算退避时间并延迟执行下一次尝试 const backoffTime = calcBackoff(retries); console.log(`Retrying in ${backoffTime}ms...`); return delay(backoffTime).then(attempt); // 延迟后递归调用 attempt } else { // 达到最大重试次数,抛出原始错误 console.error(`Max retries (${times}) reached. Failing with error:`, err); throw err; } }); } // 启动第一次尝试 return attempt(); }
delay(t: number) 函数:
kMinRetryTime 和 kPerRetryAdditionalTime:
calcBackoff(retries: number) 函数:
retry(fn: Function, params: any, times = 1e9 + 7) 函数:
设计健壮的异步重试机制是构建可靠应用的关键。核心要点包括:
遵循这些原则,开发者可以构建出
更加稳定和高效的异步操作处理逻辑。