如果你在写代码时遇到过"同时发起好几个请求,等它们都完成后再做点什么"的场景,那你一定会用到 Promise.all 或者 Promise.allSettled。这篇教程会用最简单的例子,带你一步步搞懂这两个 API 的区别、原理和实际用法。
一、先聊聊为什么需要它们
假设你要做一个用户主页,页面上需要展示:
- 用户基本信息
- 用户的订单列表
- 用户的消息通知
这三个数据分别来自三个不同的接口。如果你这样写:
javascript
const user = await fetch('/api/user');
const orders = await fetch('/api/orders');
const messages = await fetch('/api/messages');
问题来了:这三个请求是一个接一个发出去的。假设每个请求耗时 1 秒,那么总共要等 3 秒页面才能渲染完。
但其实这三个请求互不依赖 ,完全可以同时发出去,谁先回来不影响谁。这时候我们就需要"并发"请求多个 Promise,而 Promise.all 和 Promise.allSettled 就是干这个的。
二、Promise.all:一个失败,全部失败
2.1 基本用法
javascript
const p1 = fetch('/api/user');
const p2 = fetch('/api/orders');
const p3 = fetch('/api/messages');
const results = await Promise.all([p1, p2, p3]);
// results 是一个数组:[userResponse, ordersResponse, messagesResponse]
Promise.all 接收一个 Promise 数组,同时 发起所有请求,等它们全部成功后,把结果按顺序打包成一个数组返回给你。
💡 注意:返回结果的顺序和你传入数组的顺序是一致的,跟哪个请求先完成没关系。
2.2 关键特性:快速失败(Fail-Fast)
这是新手最容易踩的坑。只要数组里有任何一个 Promise 失败(reject),Promise.all 会立刻失败,哪怕其他 9 个请求都已经成功了。
来看一个例子:
javascript
function fakeRequest(name, delay, shouldFail) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldFail) {
reject(new Error(`${name} 请求失败了`));
} else {
resolve(`${name} 请求成功`);
}
}, delay);
});
}
async function run() {
try {
const results = await Promise.all([
fakeRequest('用户信息', 1000, false),
fakeRequest('订单列表', 500, true), // 这个会失败
fakeRequest('消息通知', 1500, false),
]);
console.log('全部成功:', results);
} catch (err) {
console.log('捕获到错误:', err.message);
// 输出: 捕获到错误: 订单列表 请求失败了
}
}
run();
运行结果分析:
- "订单列表"请求在 500ms 时失败了
Promise.all立刻抛出错误,不会等待"用户信息"(1000ms)和"消息通知"(1500ms)完成- 你在
catch里只能拿到第一个失败的错误信息,另外两个请求即使最终成功了,结果也被"扔掉了",你根本拿不到
2.3 什么时候用 Promise.all?
适合所有任务必须全部成功,少一个都不行的场景:
- 页面初始化时必须同时拿到"用户身份"和"权限列表",少了权限列表页面根本没法渲染
- 提交表单时需要同时上传多张图片,任何一张失败整个提交都应该失败
三、Promise.allSettled:不管成功失败,都等你说完
3.1 基本用法
javascript
const results = await Promise.allSettled([p1, p2, p3]);
Promise.allSettled 也是同时发起所有请求,但它不会因为某个请求失败就提前结束 ,而是耐心等所有 Promise 都有了结果(无论成功还是失败),然后把每一个的结果都告诉你。
3.2 返回结果的格式
这是新手第二个容易搞混的地方:allSettled 返回的数组,每一项不是 直接的结果值,而是一个带有 status 字段的对象:
javascript
// 成功的项长这样:
{ status: 'fulfilled', value: '用户信息 请求成功' }
// 失败的项长这样:
{ status: 'rejected', reason: Error('订单列表 请求失败了') }
用同样的例子跑一遍:
javascript
async function run() {
const results = await Promise.allSettled([
fakeRequest('用户信息', 1000, false),
fakeRequest('订单列表', 500, true), // 这个会失败
fakeRequest('消息通知', 1500, false),
]);
console.log(results);
/* 输出:
[
{ status: 'fulfilled', value: '用户信息 请求成功' },
{ status: 'rejected', reason: Error('订单列表 请求失败了') },
{ status: 'fulfilled', value: '消息通知 请求成功' }
]
*/
}
run();
看到了吗?即使"订单列表"失败了,"用户信息"和"消息通知"的结果我们依然拿到了。这就是 allSettled 最大的价值------它不会因为一个失败,就把其他成功的结果一起丢掉。
3.3 如何处理 allSettled 的结果
因为返回的是带 status 的对象数组,你需要手动遍历,分别处理成功和失败的情况:
javascript
const results = await Promise.allSettled([p1, p2, p3]);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`第 ${index} 个请求成功,结果是:`, result.value);
} else {
console.log(`第 ${index} 个请求失败,原因是:`, result.reason);
}
});
也可以用 filter 分别把成功和失败的结果提取出来:
javascript
const succeeded = results
.filter(r => r.status === 'fulfilled')
.map(r => r.value);
const failed = results
.filter(r => r.status === 'rejected')
.map(r => r.reason);
console.log('成功的结果:', succeeded);
console.log('失败的原因:', failed);
3.4 什么时候用 Promise.allSettled?
适合任务之间互相独立,允许部分失败的场景:
- 页面上有 3 个独立的卡片组件,各自请求各自的数据,其中一个接口挂了,不应该影响另外两个卡片正常显示
- 批量上传多个文件,需要告诉用户"哪些成功了,哪些失败了",而不是一失败就全部作废
- 埋点、日志上报等允许失败、但不能因为失败而中断主流程的场景
四、两者对比总结
| 对比项 | Promise.all |
Promise.allSettled |
|---|---|---|
| 何时结束 | 任意一个 reject 就立刻结束 | 等所有 Promise 都有结果才结束 |
| 一个失败,其他会怎样 | 其他的结果全部丢失,拿不到 | 其他成功/失败的结果都会保留 |
| 返回值格式 | 直接是结果值数组 | {status, value} 或 {status, reason} 的对象数组 |
| 需要 try/catch 吗 | 需要,失败会抛异常 | 不需要,永远 resolve,自己判断 status |
| 典型场景 | 强依赖,少一个都不行 | 弱依赖,允许部分失败 |
一句话记忆法:
all是"团灭模式"------一个倒下,全部阵亡;allSettled是"点名模式"------不管死活,每个人都要报到。
五、实战:一个更接近真实业务的例子
假设你要做一个仪表盘页面,同时请求 3 个统计接口来渲染 3 张图表卡片,任何一张图表挂了都不该影响另外两张正常展示:
typescript
interface CardData {
title: string;
data: unknown;
error: string | null;
}
async function loadDashboard(): Promise<CardData[]> {
const requests = [
{ title: '销售额趋势', fetcher: () => fetch('/api/sales').then(r => r.json()) },
{ title: '用户增长', fetcher: () => fetch('/api/users').then(r => r.json()) },
{ title: '订单统计', fetcher: () => fetch('/api/orders').then(r => r.json()) },
];
const results = await Promise.allSettled(
requests.map(req => req.fetcher())
);
// 把 settled 结果转换成统一的、方便渲染的格式
return results.map((result, index) => ({
title: requests[index].title,
data: result.status === 'fulfilled' ? result.value : null,
error: result.status === 'rejected' ? String(result.reason) : null,
}));
}
这样一来,即使"订单统计"接口挂了,页面依然能正常渲染"销售额趋势"和"用户增长"两张卡片,只在"订单统计"的位置显示一个错误提示,而不是整个仪表盘白屏。
六、新手常踩的 3 个坑
坑 1:以为 allSettled 会像 all 一样抛错
javascript
// ❌ 错误认知:以为失败了会走 catch
try {
const results = await Promise.allSettled([p1, p2]);
} catch (err) {
// 这里永远不会被触发!allSettled 不会 reject
}
allSettled 永远不会 reject ,你必须手动检查每一项的 status,漏检查就等于"悄悄吞掉了错误"。
坑 2:用 Promise.all 却没做好整体降级处理
如果你的业务场景其实允许部分失败,却用了 Promise.all,一旦某个请求偶发失败,就会导致整个功能不可用,体验非常差。先想清楚业务上是"强依赖"还是"弱依赖",再选 API。
坑 3:忘记超时控制
无论是 all 还是 allSettled,如果某个 Promise 一直不 resolve/reject(比如网络长时间无响应),都会一直卡住不返回。生产环境建议自己包一层超时:
typescript
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
const timeout = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error('请求超时')), ms);
});
return Promise.race([promise, timeout]);
}
// 使用时给每个请求都包一层超时
const results = await Promise.allSettled([
withTimeout(fetch('/api/sales'), 5000),
withTimeout(fetch('/api/users'), 5000),
]);
七、总结
Promise.all:适合"必须全成功"的强依赖场景,一个失败全部作废,写法简单(直接try/catch即可)。Promise.allSettled:适合"允许部分失败"的弱依赖场景,不管成功失败都会等你,需要自己遍历判断status。