在 JavaScript 里让两个 fetch 并发请求同时发出、再一起拿到结果,标准做法是 Promise.all:把两个请求放进数组一次性发起,用一次 await 收齐。想允许其中一个失败,改用 Promise.allSettled。真正的坑是有人写成两次顺序 await,那不是并发,是串行,耗时等于两者之和。

初学者最容易踩的是后面这个坑。代码看起来很「同步」,写起来也自然,但它把两个本可以并行的网络请求变成了排队执行,页面加载时间直接翻倍。而且因为没有对比过耗时,这个性能问题往往上线很久才被发现。
本文给出三种写法、一份横向对比表、超时控制的做法,以及判断自己写的是并发还是串行的具体方法。今天这篇文章,编程狮就把这块讲透。
先看结论
两个 fetch 要并发执行并一起拿结果,用 Promise.all;要允许部分失败,用 Promise.allSettled;顺序 await 是串行写法,不该出现在并发场景里。
判断并发还是串行,看发起时机:两个 fetch 调用是否在同一轮同步代码里就都执行了。只要第二个 fetch 写在第一个 await 之后,它就是串行的。
异步这块若不牢,可以先翻 JavaScript 教程 把 Promise 的三种状态过一遍,再回来看这三种写法。
一、先分清「并发」和「同步等待」
这两个词说的是两件事,混在一起就容易写错。
1.1 并发指的是发起时机
并发是说两个请求几乎同时发出去,网络层面重叠,总耗时接近两者中较慢的那个。
1.2 同步等待指的是收结果的方式
同步等待是说你用 await 把结果收齐再往下走,代码读起来像同步代码。它不关心两个请求是怎么发出的。
所以「并发 + 同步等待」的正确组合是:先一次性发起,再一次性等待。写成「发起 → 等待 → 再发起 → 再等待」,就变成了串行。
| 写法 | 发起方式 | 总耗时 | 一个失败时 |
|---|---|---|---|
| Promise.all | 并发 | 取较慢者 | 整体失败 |
| Promise.allSettled | 并发 | 取较慢者 | 其余照常返回 |
| 顺序 await | 串行 | 两者之和 | 中断后续 |
二、方法一:Promise.all,都成功才继续
两个请求必须都成功才有意义时,用这个。
async function loadBoth(userId) {
const [user, order] = await Promise.all([
fetch(`https://www.w3cschool.cn/api/user/${userId}`).then((r) => r.json()),
fetch(`https://www.w3cschool.cn/api/order/${userId}`).then((r) => r.json())
]);
return { user, order };
}
loadBoth(42).then(console.log).catch(console.error);
上面这段代码做的是:两个 fetch 在数组里被同时构造并发出,Promise.all 等两个都完成后按数组顺序返回结果,再用解构分别取出。注意 .then((r) => r.json()) 不能漏,忘了这步拿到的是 Response 对象而不是数据。
⚠️ 注意:
Promise.all是「一失败全失败」。只要其中一个 reject,整个 Promise 就 reject,另一个请求的结果也拿不到。需要部分成功时不要用这个。
三、方法二:Promise.allSettled,允许部分失败
两个请求互相独立、一个挂了也不能影响另一个时,用这个。
async function loadBothSafe(userId) {
const results = await Promise.allSettled([
fetch(`https://www.w3cschool.cn/api/user/${userId}`).then((r) => r.json()),
fetch(`https://www.w3cschool.cn/api/order/${userId}`).then((r) => r.json())
]);
const picked = results.map((item) =>
item.status === 'fulfilled' ? item.value : { error: String(item.reason) }
);
return { user: picked[0], order: picked[1] };
}
上面这段代码做的是:等两个请求都结束(无论成功失败),逐个判断状态,成功取 value,失败把原因转成字符串。这样界面上可以只降级显示失败的那一块。
💡 小提示:失败原因转成字符串再往上抛,比直接抛原始错误更容易在界面上展示,也避免把内部堆栈暴露给用户。
四、超时控制与三个高频坑
并发写法本身不难,难在边界。
4.1 加超时:用 AbortController
网络请求没有默认超时,一个卡住的请求会让 Promise.all 永远挂着。
async function fetchWithTimeout(url, ms) {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), ms);
try {
const resp = await fetch(url, { signal: ctrl.signal });
return await resp.json();
} finally {
clearTimeout(timer);
}
}
上面这段代码做的是:创建一个中止控制器,设定毫秒后触发中止,把信号传给 fetch,无论成功失败都在最后清掉定时器。预期结果(未执行,依据 AbortController 的标准行为)是超时时抛出一个中止错误。
4.2 坑一:顺序 await 当成并发
修复方式是把两个 fetch 调用放进同一个数组,不要在中间 await。
4.3 坑二:忘了处理 HTTP 错误状态
fetch 只在网络层面失败时 reject,404、500 这类状态码不会触发 reject。需要自己判断 resp.ok。
4.4 坑三:在循环里 await
循环里逐个 await 是典型的串行写法,请求多了耗时会线性增长。批量场景仍然要用 Promise.all,但要控制并发数量,避免一次性打出几百个请求。

4.5 本地跑一遍,验证到底是不是并发
只靠读代码判断并发不可靠,跑一次测耗时最直观。下面这段做的是分别用两种写法各跑一遍并打印耗时。
async function bench(userId) {
const t0 = Date.now();
await Promise.all([
fetch(`https://www.w3cschool.cn/api/user/${userId}`).then((r) => r.json()),
fetch(`https://www.w3cschool.cn/api/order/${userId}`).then((r) => r.json())
]);
console.log('Promise.all 耗时', Date.now() - t0, 'ms');
const t1 = Date.now();
await fetch(`https://www.w3cschool.cn/api/user/${userId}`).then((r) => r.json());
await fetch(`https://www.w3cschool.cn/api/order/${userId}`).then((r) => r.json());
console.log('顺序 await 耗时', Date.now() - t1, 'ms');
}
上面这段代码做的是分别测量两种写法的总耗时。预期结果(未执行,依据并发与串行的执行模型)是顺序写法的耗时接近两次请求之和,并发写法接近其中较慢的那一次。
在 Node 环境里运行这段脚本的命令如下(预期结果:打印两组耗时数字)。
node -v
node bench.js
上面两条命令做的是先检查 Node 版本,再执行脚本。本机只需要安装 Node 运行环境,不需要额外依赖;预期结果(未执行)是终端打印出两组毫秒数,两组差距明显就说明并发写法确实生效了。
总结
两个 fetch 并发请求要一起收结果,首选 Promise.all;允许部分失败就用 Promise.allSettled;顺序 await 是串行写法,别用在并发场景。另外记得补超时与 HTTP 状态判断,这两项是线上事故的高发点。
要点带走:
- 判断并发看发起时机,第二个 fetch 写在第一个 await 之后就是串行;
Promise.all一失败全失败,需要部分成功要换成Promise.allSettled;fetch不会因 404、500 而 reject,状态码要自己判断。
下一步建议给自己封装一个带超时和状态检查的请求函数,后面所有页面都复用它。语法细节记不住时,翻 JavaScript 速查手册 最快。

常见问题
Q:Promise.all 和 Promise.allSettled 该怎么选?
A:两个请求必须都成功才继续,用 Promise.all;一个挂了也要展示另一块,用 allSettled。
Q:为什么 fetch 返回 404 但代码没进 catch?
A:fetch 只在网络层面失败时 reject,HTTP 错误状态码不算失败。需要自己判断 resp.ok 并主动抛错。
Q:并发请求数量很多怎么办?
A:一次性打出几百个请求会拖垮服务端,也可能触发浏览器限制。需要自己实现分批并发,控制同时在飞的请求数。
Q:怎么确认自己写的是并发不是串行?
A:看两个 fetch 是否在同一轮同步代码里被构造。也可以在控制台计时,串行耗时约等于两者之和。
延伸学习
想把异步这块补齐,可以按这个顺序来:
- 读一篇相关笔记 JavaScript 异步编程,把 Promise 这条线理顺;
- 想动手练,前端配套课程 是边学边写的形式;
- 想再往网络层深入,可以看 WebSocket 协议教程。

TRAE-AI编程



