JavaScript递归重试请求时程序提前退出原因排查
核心问题:Promise逻辑错误导致提前退出
你的requestUntilSucceed函数存在致命逻辑问题:当第一次请求失败进入catch分支时,你调用了递归的requestUntilSucceed,但外层的Promise并没有等待这个递归操作完成,而是直接执行了.then((resp) => resolve(attempt)),把第一次的尝试次数作为结果resolve了。
这就导致Promise.all会认为这些“失败后重试”的请求已经成功完成,当所有300个请求对应的Promise都被resolve后,程序就会提前退出,而那些还在重试的请求实际还在后台运行,但已经没人等待它们了。
为什么recursionBypass没用?
这个函数只是简单地直接调用传入的函数,没有做任何异步调度,递归依然是同步执行的——当重试次数足够多时,依然会触发栈溢出,但这不是你这次程序提前退出的原因,核心还是Promise的错误处理。
修复方案
1. 修正requestUntilSucceed的Promise链
不要手动包裹新的Promise,利用Promise链式调用的特性,让失败后的递归请求成为Promise链的一部分,确保只有当最终请求成功时,整个Promise才会resolve:
function sendRequest(url, body, isGood) { return fetch(url, body) .then(resp => { if (isGood(resp)) { return resp; } else { throw resp.status; } }); } function requestUntilSucceed(url, body, isGood, name, attempt = 1) { return sendRequest(url, body, isGood) .then(() => attempt) // 请求成功,返回尝试次数 .catch(() => { // 失败后递归调用,返回新的Promise,让整个链继续等待 return requestUntilSucceed(url, body, isGood, name, attempt + 1); }); }
2. 避免递归栈溢出
如果服务器冷却时间长,重试次数会非常多,同步递归很快会触发栈溢出。可以通过异步调度(比如setTimeout)把下一次重试放到新的事件循环中,同时也能避免短时间内发送大量请求:
function requestUntilSucceed(url, body, isGood, name, attempt = 1) { return sendRequest(url, body, isGood) .then(() => attempt) .catch(() => { // 加1秒延迟后重试,可根据实际情况调整 return new Promise(resolve => setTimeout(resolve, 1000)) .then(() => requestUntilSucceed(url, body, isGood, name, attempt + 1)); }); }
3. 修正测试代码的语法错误
你的测试代码中await function()写法错误,需要改成异步立即执行函数,同时不需要额外包裹一层Promise:
(async function() { let promises = []; for(let i=0; i<300; i++) { promises.push( createRequest(requestArg, i) .then((attempt) => { console.log("Request: ", i, "succeeded on attempt: " + attempt); return attempt; }) ); }; await Promise.all(promises); })();
总结
程序提前退出的根本原因是requestUntilSucceed错误地提前resolve了Promise,和所谓的“未知JavaScript限制”无关。修正Promise链式逻辑后,Promise.all会正确等待所有请求最终成功后才结束程序,加上异步调度也能避免递归栈溢出的问题。
内容的提问来源于stack exchange,提问作者Ozz

