递归XHR Promise触发404无调用栈,重试逻辑异常原因排查
问题:POST请求重试函数达到最大次数后未正常拒绝,抛出未捕获异常
我写了一个发起POST请求并返回Promise的postToServer函数,代码运行正常:
function postToServer(url, body) { let xhr = new XMLHttpRequest(); xhr.open(`POST`, url, true); xhr.setRequestHeader(`Content-type`, `application/json`); return new Promise((resolve, reject) => { xhr.onload = () => { if(xhr.readyState == XMLHttpRequest.DONE && xhr.status == 200) { resolve(); } else { reject(xhr.status); } }; xhr.send(body); }); }
但在实现失败重试逻辑的postToServerAndRetryOnFail函数时遇到了问题:
function postToServerAndRetryOnFail(url, body, failedAttempts = 0) { return new Promise((resolve, reject) => { postToServer(url, body).then(() => { resolve(); }, statusCode => { failedAttempts++; if(failedAttempts <= maxAttempts) { wait(intervalBetweenAttempts).then(() => { postToServerAndRetryOnFail(url, body, failedAttempts); }); } else { reject(statusCode); } }); }); }
预期逻辑是请求失败时自动重试,直到达到最大次数后返回拒绝状态,但实际情况是:重试至最大次数后并未正常拒绝,而是抛出uncaught exception: 404错误且无调用栈。请问这是什么原因?
解答
这个问题的核心是你在递归调用时没有把内层Promise的结果和外层的Promise关联起来,导致外层Promise一直处于pending状态,而递归里的reject变成了未被捕获的异常。具体来说:
- 你在外层手动新建了一个Promise,但当你递归调用
postToServerAndRetryOnFail时,只是调用了函数,却没有把这个递归返回的Promise的resolve/reject和外层Promise的resolve/reject绑定。这就导致当递归到最后一次失败时,那个reject没有被任何Promise链捕获,直接变成了未处理的异常。 - 另外,你的代码里的错误处理分支没有正确传递Promise的状态,外层Promise根本不知道递归里发生了什么。
修复后的代码
我们可以去掉外层手动创建的Promise,直接返回postToServer的Promise链式调用,这样递归的结果会自动融入Promise链:
// 假设maxAttempts和intervalBetweenAttempts是全局或传入的变量 function postToServerAndRetryOnFail(url, body, failedAttempts = 0) { return postToServer(url, body) .catch(statusCode => { failedAttempts++; if (failedAttempts <= maxAttempts) { // 等待后返回递归调用的Promise,让链延续 return wait(intervalBetweenAttempts) .then(() => postToServerAndRetryOnFail(url, body, failedAttempts)); } else { // 达到最大次数,直接拒绝,这个拒绝会被上层Promise链捕获 return Promise.reject(statusCode); } }); }
关键改动说明
- 去掉了外层的
new Promise:Promise链式调用本身就可以返回新的Promise,不需要手动包裹,这能避免"Promise嵌套陷阱"。 - 重试时返回递归调用的
postToServerAndRetryOnFail:这样递归的Promise状态会被传递到当前的Promise链中,上层的调用者能正确捕获到最终的resolve或reject。 - 用
catch替代了then的第二个参数:这是更清晰的错误处理方式,也能避免遗漏一些潜在的错误。
这样修改后,当达到最大重试次数时,Promise.reject(statusCode)会被正确传递到最上层的调用者,不会再出现未捕获的异常。
内容的提问来源于stack exchange,提问作者Ian
相关产品推荐
相关产品推荐

