AbortSignal.timeout 本身只中止请求,需与 fallback 请求组合实现降级;降级是主请求失败(含超时)后立即发起更轻量、语义一致的替代请求,而非重试。
直接用 AbortSignal.timeout 本身不能实现降级,它只负责中止请求;但把它和“失败后 fallback 请求”组合起来,就能构成真正可用的自动降级加载策略——核心是把超时当作一种可预期的失败分支,而非异常。
很多团队误把降级写成“超时就再 fetch 一次”,这实际是重试,不仅加重后端压力,还掩盖了真实问题。真正的降级应满足:
以下是一个生产就绪的封装函数,支持主请求超时后自动切换到降级接口:
async function loadWithFallback(url, fallbackUrl, options = {}) { const { timeout = 5000, fallbackTimeout = 2000 } = options; try { // 主请求带超时 const res = await fetch(url, { signal: AbortSignal.timeout(timeout), ...options }); if (!res.ok) throw new Error(`HTTP ${res.status}`); return await res.json(); } catch (err) { // 仅对 AbortError(即超时)触发降级,其他错误(如网络断开)不降级 if (err.name !== 'AbortError') throw err; // 降级请求独立发起,不共享 signal,避免二次超时干扰 try { const fallbackRes = await fetch(fallbackUrl, { signal: AbortSignal.timeout(fallbackTimeout), ...options }); if (fallbackRes.ok) return await fallbackRes.json(); } catch (fallbackErr) { // 降级也失败?抛出原始超时错误,保留根因 if (fallbackErr.name === 'AbortError') { throw new Error('主请求与降级请求均超时'); } } // 降级返回非 2xx?仍尝试解析,保持结构兼容 throw new Error('降级请求失败,返回非成功状态'); }}
AbortError 触发降级,TypeError(如网络断开)、NetworkError 等应原样抛出,避免掩盖真实故障"avatar": null),不可删 key 或改类型Promise.race 或标记位控制最终 resolve 的时机,防止“慢响应覆盖快降级”当加载还需响应用户主动取消(如点击“停止加载”按钮)或页面即将卸载时,可将 timeout 与其他信号聚合:
const controller = new AbortController();const timeoutCtrl = new AbortController();const userCancelCtrl = new AbortController();// 超时控制setTimeout(() => timeoutCtrl.abort(), 5000);// 用户取消cancelBtn.addEventListener('click', () => userCancelCtrl.abort());// 聚合信号(注意:仅用于主请求)const mainSignal = AbortSignal.any([ controller.signal, timeoutCtrl.signal, userCancelCtrl.signal]);// 主请求用聚合信号,降级请求仍用独立 timeoutfetch(url, { signal: mainSignal }) .then(r => r.json()) .catch(err => { if (err.name === 'AbortError') { // 判断是否为 timeout 引起(需自行记录原因,见下文) return loadFallback(fallbackUrl); } });
注意:AbortSignal.any 不暴露哪个信号先触发,如需精准判断是否因超时降级,建议在 setTimeout 回调中设一个标志位(如 isTimeoutTriggered = true),并在 catch 中检查。