Bạn gọi 5 API song song bằng Promise.all, một request fail — cả 5 kết quả bay màu, UI trắng trơn. Không phải Promise.all bị lỗi, mà là bạn chọn sai công cụ.
JavaScript có bốn static combinator để gom nhiều promise lại: all, allSettled, race, any. Mỗi cái một chiến lược thắng/thua khác nhau — cái reject ngay khi có một lỗi, cái không bao giờ reject, cái chỉ cần đứa nhanh nhất. Đa số dev chỉ biết đúng một cái và dùng nó cho mọi tình huống. Bài này chỉ bạn chọn đúng.
1. Nền tảng nhanh trước khi vào combinator
Trước khi so sánh bốn combinator, cần thống nhất vài khái niệm. Một Promise luôn ở một trong ba trạng thái: pending (chưa xong), fulfilled (thành công, có value), hoặc rejected (thất bại, có reason). Khi rời khỏi pending sang một trong hai trạng thái còn lại, ta nói promise đã settle — và settle rồi thì không đổi được nữa.
Từ “settle” quan trọng vì mấy combinator bên dưới định nghĩa hành vi theo nó: có cái chờ tất cả settle, có cái chỉ chờ cái đầu tiên settle.
1
2
3
4
5
6
7
8
9
10
// Ba cách tạo promise ở trạng thái đã biết
const ok = Promise.resolve(42); // fulfilled ngay, value = 42
const bad = Promise.reject(new Error("x")); // rejected ngay, reason = Error
ok.then((v) => console.log("fulfilled:", v));
bad.catch((e) => console.log("rejected:", e.message));
// Một object có method .then() (thenable) cũng được await/then như promise
const thenable = { then: (resolve) => resolve("giống promise") };
Promise.resolve(thenable).then((v) => console.log(v));Bốn combinator dưới đây đều nhận một iterable các promise (thường là array) và trả về một promise mới. Khác biệt duy nhất là: promise mới đó settle khi nào và mang giá trị gì. Bài này không nhắc lại thứ tự chạy giữa microtask và macrotask — nếu bạn thắc mắc vì sao Promise chạy trước setTimeout(0), mình đã viết riêng trong bài Tại sao Promise chạy trước setTimeout(0) và bài Macro task queue và hiệu suất.
Để demo mà không phụ thuộc mạng, mình dùng hai helper nhỏ giả lập độ trễ. Chúng lặp lại ở đầu mỗi ví dụ vì mỗi nút Run chạy độc lập:
1
2
3
4
5
6
7
8
9
10
// resolve sau ms mili-giây với value
function delay(ms, value) {
return new Promise((resolve) => setTimeout(() => resolve(value), ms));
}
// reject sau ms mili-giây với một Error
function failAfter(ms, reason) {
return new Promise((_, reject) => setTimeout(() => reject(new Error(reason)), ms));
}
console.log("Hai helper đã sẵn sàng cho các ví dụ bên dưới");2. Promise.all: được ăn cả, ngã về không
Promise.all là combinator quen nhất và cũng bị dùng sai nhiều nhất. Nó fail-fast: reject ngay lập tức khi một promise bất kỳ reject, mang theo đúng reason của promise đó. Chỉ khi tất cả fulfilled, nó mới fulfilled — với một array kết quả.
Điểm nhiều người nhầm: array kết quả giữ đúng thứ tự input, không phải thứ tự hoàn thành.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function delay(ms, value) {
return new Promise((resolve) => setTimeout(() => resolve(value), ms));
}
async function main() {
const results = await Promise.all([
delay(300, "user"), // xong trễ nhất
delay(100, "posts"), // xong sớm nhất
delay(200, "comments"),
]);
// Kết quả theo thứ tự bạn truyền vào, KHÔNG theo thứ tự settle
console.log(results); // ["user", "posts", "comments"]
}
main();Nhờ đặc điểm đó, bạn destructure kết quả an toàn theo vị trí: const [user, posts, comments] = await Promise.all([...]). Đây là lý do Promise.all hợp cho việc gom dữ liệu bắt buộc phải đủ — ví dụ một trang cần cả user, cả bài viết, cả bình luận thì mới render được.
2.1. Fail-fast không có nghĩa là “hủy”
Cái bẫy nằm ở đây: khi Promise.all reject, các promise còn lại vẫn chạy tiếp. JavaScript không có cơ chế cancel một promise đang chạy; Promise.all chỉ đơn giản là ngừng chờ chúng, chứ không dừng được công việc bên dưới.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function delay(ms, value) {
return new Promise((resolve) => setTimeout(() => resolve(value), ms));
}
function failAfter(ms, reason) {
return new Promise((_, reject) => setTimeout(() => reject(new Error(reason)), ms));
}
async function main() {
const track = (name, p) =>
p.then(
(v) => { console.log(`✓ ${name} xong`); return v; },
(e) => { console.log(`✗ ${name} lỗi`); throw e; }
);
try {
await Promise.all([
track("A", delay(100, "A")),
track("B", failAfter(200, "B sập")), // reject ở mốc 200ms
track("C", delay(500, "C")), // vẫn còn chạy khi all đã reject
]);
} catch (err) {
console.log("Promise.all reject:", err.message);
}
await delay(600);
console.log("C vẫn chạy tới cùng dù all đã bỏ cuộc từ mốc 200ms");
}
main();Chạy thử: A xong ở 100ms, B lỗi ở 200ms khiến all reject ngay, nhưng dòng ✓ C xong vẫn in ra ở mốc 500ms. Trong thực tế điều này nghĩa là: ba request fetch gửi qua Promise.all, một cái fail, thì hai cái kia vẫn tiêu tốn băng thông và vẫn ghi dữ liệu ở server. Muốn dừng thật thì phải dùng AbortController — mình nói kỹ ở mục 4.2 bên dưới.
3. Promise.allSettled: khi một lỗi không được phép kéo cả nhóm
Promise.allSettled là câu trả lời cho đúng vấn đề ở đầu bài. Nó không bao giờ reject: chờ tất cả promise settle rồi trả về một array mô tả kết quả từng cái. Mỗi phần tử có shape cố định:
- Fulfilled:
{ status: "fulfilled", value: <giá trị> } - Rejected:
{ status: "rejected", reason: <lý do> }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function delay(ms, value) {
return new Promise((resolve) => setTimeout(() => resolve(value), ms));
}
function failAfter(ms, reason) {
return new Promise((_, reject) => setTimeout(() => reject(new Error(reason)), ms));
}
async function main() {
const results = await Promise.allSettled([
delay(100, "email gửi ok"),
failAfter(150, "SMS timeout"),
delay(200, "push gửi ok"),
]);
// reason là chính Error object; ở đây in .message cho gọn console
console.log(
results.map((r) =>
r.status === "fulfilled"
? { status: r.status, value: r.value }
: { status: r.status, reason: r.reason.message }
)
);
}
main();Vì shape luôn có status, bạn tách thành công / thất bại bằng filter:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function delay(ms, value) {
return new Promise((resolve) => setTimeout(() => resolve(value), ms));
}
function failAfter(ms, reason) {
return new Promise((_, reject) => setTimeout(() => reject(new Error(reason)), ms));
}
async function main() {
const results = await Promise.allSettled([
delay(100, "email gửi ok"),
failAfter(150, "SMS timeout"),
delay(200, "push gửi ok"),
]);
const sent = results
.filter((r) => r.status === "fulfilled")
.map((r) => r.value);
const failed = results
.filter((r) => r.status === "rejected")
.map((r) => r.reason.message);
console.log("Thành công:", sent); // ["email gửi ok", "push gửi ok"]
console.log("Thất bại:", failed); // ["SMS timeout"]
}
main();Chọn allSettled khi các tác vụ độc lập với nhau và bạn cần biết kết quả từng cái: gửi N notification (một cái lỗi không nên chặn N-1 cái còn lại), load N widget trên dashboard, hay chạy một batch job rồi báo cáo cái nào pass cái nào fail. Đây là mặc định an toàn hơn Promise.all cho các batch mà một phần lỗi vẫn chấp nhận được.
4. Promise.race: ai settle trước, người đó thắng
Promise.race settle theo promise đầu tiên settle — bất kể fulfilled hay rejected. Nếu cái tới đích trước là một reject, thì race cũng reject theo. “Race” ở đây là đua settle, không phải đua thành công (đua thành công là việc của Promise.any ở mục 5).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function delay(ms, value) {
return new Promise((resolve) => setTimeout(() => resolve(value), ms));
}
function failAfter(ms, reason) {
return new Promise((_, reject) => setTimeout(() => reject(new Error(reason)), ms));
}
async function main() {
try {
const winner = await Promise.race([
delay(300, "chậm nhưng ok"),
failAfter(100, "lỗi tới sớm"), // reject sớm hơn → race reject theo
]);
console.log("Winner:", winner);
} catch (err) {
console.log("Race reject:", err.message); // "lỗi tới sớm"
}
}
main();4.1. Pattern timeout kinh điển
Ứng dụng phổ biến nhất của race là timeout: cho một tác vụ đua với một promise reject sau X mili-giây. Ai xong trước thắng — nếu tác vụ chậm hơn deadline thì timeout thắng và bạn nhận được lỗi.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function delay(ms, value) {
return new Promise((resolve) => setTimeout(() => resolve(value), ms));
}
function timeout(ms) {
return new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Timeout sau ${ms}ms`)), ms)
);
}
async function main() {
try {
const data = await Promise.race([
delay(500, "dữ liệu"), // API mất 500ms
timeout(200), // ta chỉ chờ tối đa 200ms
]);
console.log("Nhận được:", data);
} catch (err) {
console.log(err.message); // "Timeout sau 200ms"
}
}
main();Pattern này chạy được nhưng có một lỗ hổng giống mục 2.1: khi timeout thắng, cái delay(500) vẫn tiếp tục chạy tới cùng. Với fetch, request thật vẫn đi tiếp và tốn tài nguyên.
4.2. Dọn dẹp thật bằng AbortSignal.timeout()
Muốn timeout và dừng hẳn công việc bên dưới, hãy dùng AbortController / AbortSignal.timeout() (một static method trả về signal tự abort sau ms mili-giây). Khi signal abort, ta chủ động hủy tác vụ và reject bằng signal.reason — một DOMException tên TimeoutError.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function slowTask(signal) {
return new Promise((resolve, reject) => {
const id = setTimeout(() => resolve("dữ liệu"), 500);
// Khi hết giờ: dừng hẳn timer thay vì để nó chạy vô ích
signal.addEventListener("abort", () => {
clearTimeout(id);
reject(signal.reason);
});
});
}
async function main() {
const signal = AbortSignal.timeout(200); // tự abort sau 200ms
try {
const data = await slowTask(signal);
console.log("Nhận được:", data);
} catch (err) {
console.log("Hủy vì:", err.name); // "TimeoutError"
}
}
main();Với fetch, bạn truyền thẳng signal vào request và không cần tự viết race — trình duyệt tự abort connection khi hết giờ:
1
2
3
4
5
6
// AbortSignal.timeout() hủy luôn request thật, không chỉ bỏ kết quả
async function loadWithTimeout(url) {
const res = await fetch(url, { signal: AbortSignal.timeout(3000) });
return res.json();
// Quá 3s: fetch reject với DOMException name = "TimeoutError"
}Nói ngắn gọn: Promise.race hợp cho “đua để lấy đứa nhanh nhất, chấp nhận cả kết quả xấu”, còn timeout có dọn dẹp thì ưu tiên AbortSignal.
5. Promise.any: chỉ cần một cái thành công
Promise.any là bản “lạc quan” của race: nó fulfilled ngay khi có promise đầu tiên fulfilled, và bỏ qua các reject dọc đường. Nó chỉ reject khi tất cả đều reject — và khi đó reason là một AggregateError gom toàn bộ lỗi lại trong thuộc tính .errors.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function delay(ms, value) {
return new Promise((resolve) => setTimeout(() => resolve(value), ms));
}
function failAfter(ms, reason) {
return new Promise((_, reject) => setTimeout(() => reject(new Error(reason)), ms));
}
async function main() {
// 3 mirror; cái reject sớm bị bỏ qua, cái fulfilled đầu tiên thắng
const fastest = await Promise.any([
failAfter(100, "mirror-1 sập"), // lỗi ở 100ms → any phớt lờ
delay(200, "mirror-2 trả dữ liệu"), // fulfilled ở 200ms → thắng
delay(400, "mirror-3 trả dữ liệu"),
]);
console.log("Dùng:", fastest); // "mirror-2 trả dữ liệu"
}
main();So sánh nhanh với mục 4: nếu đây là Promise.race, mirror-1 sập reject ở 100ms sẽ khiến cả cụm reject. Promise.any thì kiên nhẫn chờ một cái thành công. Đúng use case cho fallback: gọi nhiều mirror / endpoint dự phòng, lấy cái nào trả lời trước.
5.1. Đọc AggregateError khi mọi cái đều fail
Khi mọi endpoint đều fail, catch nhận AggregateError. Đừng đọc .message (chỉ là “All promises were rejected”), hãy duyệt .errors:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function failAfter(ms, reason) {
return new Promise((_, reject) => setTimeout(() => reject(new Error(reason)), ms));
}
async function main() {
try {
await Promise.any([
failAfter(100, "endpoint A chết"),
failAfter(150, "endpoint B chết"),
failAfter(200, "endpoint C chết"),
]);
} catch (err) {
console.log("Là AggregateError:", err instanceof AggregateError); // true
console.log("message:", err.message); // "All promises were rejected"
// .errors giữ đủ mọi lý do reject, theo thứ tự input
for (const e of err.errors) {
console.log("-", e.message);
}
}
}
main();6. Bảng so sánh: chọn cái nào cho tình huống nào
Bốn combinator gói gọn lại theo bốn câu hỏi:
| Combinator | Fulfilled khi | Rejected khi | Shape kết quả | Dùng khi |
|---|---|---|---|---|
Promise.all | tất cả fulfilled | một cái reject (fail-fast) | array value đúng thứ tự input | cần đủ mọi kết quả mới đi tiếp |
Promise.allSettled | luôn fulfilled (khi tất cả settle) | không bao giờ | array {status, value/reason} | batch độc lập, cần biết từng cái |
Promise.race | cái đầu tiên settle là fulfilled | cái đầu tiên settle là reject | value/reason của cái đầu tiên | timeout, lấy đứa nhanh nhất |
Promise.any | cái đầu tiên fulfilled | tất cả reject (AggregateError) | value của cái fulfilled đầu tiên | fallback nhiều nguồn dự phòng |
Cùng một câu hỏi, dạng sơ đồ quyết định:
View Mermaid diagram code
flowchart TD
Start{"Cần gì từ N promise?"} --> AllOk["Cần TẤT CẢ thành công"]
Start --> EachResult["Cần biết kết quả TỪNG cái"]
Start --> FirstAny["Chỉ cần cái ĐẦU TIÊN settle"]
Start --> FirstOk["Chỉ cần cái ĐẦU TIÊN thành công"]
AllOk --> All["Promise.all"]
EachResult --> AllSettled["Promise.allSettled"]
FirstAny --> Race["Promise.race"]
FirstOk --> Any["Promise.any"]
style All fill:#1a2c08,stroke:#c7ff50,stroke-width:1.5px,color:#c7ff50
style AllSettled fill:#0e2233,stroke:#4aa8ff,stroke-width:1.5px,color:#9ecbff
style Race fill:#2a2008,stroke:#ffc043,stroke-width:1.5px,color:#ffd98a
style Any fill:#241832,stroke:#c08cff,stroke-width:1.5px,color:#d4b3ff7. Song song hay tuần tự — và đừng bắn vào chân
Có combinator rồi, lỗi kinh điển tiếp theo là quên dùng chúng: await từng cái một trong khi đáng lẽ chạy song song. Mỗi await chặn dòng tiếp theo tới khi promise trước xong, nên thời gian cộng dồn thay vì chồng lên nhau.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function delay(ms, value) {
return new Promise((resolve) => setTimeout(() => resolve(value), ms));
}
async function main() {
// ❌ Tuần tự: b chờ a, c chờ b → cộng dồn ~300ms
const t1 = performance.now();
await delay(100, "a");
await delay(100, "b");
await delay(100, "c");
console.log(`Tuần tự: ${Math.round(performance.now() - t1)}ms`);
// ✅ Song song: cả ba khởi động cùng lúc → ~100ms
const t2 = performance.now();
await Promise.all([delay(100, "x"), delay(100, "y"), delay(100, "z")]);
console.log(`Song song: ${Math.round(performance.now() - t2)}ms`);
}
main();Chênh lệch ~300ms so với ~100ms chỉ với ba tác vụ. Với N request độc lập, tuần tự chậm gấp N lần một cách vô nghĩa. Nguyên tắc: cứ khởi động hết các promise trước, rồi mới await Promise.all([...]).
7.1. Nhưng đừng song song 1000 cái cùng lúc
Promise.all(1000 request) lại là thái cực ngược: bạn nện 1000 connection vào server (hoặc bị rate-limit, hoặc sập chính máy mình). Giải pháp là giới hạn concurrency — chạy tối đa K cái một lúc. Không cần thư viện, một pLimit mini khoảng 15 dòng là đủ:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
function delay(ms, value) {
return new Promise((resolve) => setTimeout(() => resolve(value), ms));
}
// Chạy tối đa `limit` tác vụ cùng lúc; số còn lại xếp hàng
function pLimit(limit) {
let active = 0;
const queue = [];
const next = () => {
if (active >= limit || queue.length === 0) return;
active++;
const { fn, resolve, reject } = queue.shift();
fn().then(resolve, reject).finally(() => {
active--;
next(); // một slot trống → kéo tác vụ kế tiếp
});
};
return (fn) =>
new Promise((resolve, reject) => {
queue.push({ fn, resolve, reject });
next();
});
}
async function main() {
const limit = pLimit(2); // tối đa 2 tác vụ chạy song song
let running = 0;
let peak = 0;
const task = (id) => async () => {
running++;
peak = Math.max(peak, running);
await delay(100, id);
running--;
return id;
};
const ids = [1, 2, 3, 4, 5, 6];
const results = await Promise.all(ids.map((id) => limit(task(id))));
console.log("Kết quả:", results); // [1, 2, 3, 4, 5, 6] — vẫn đúng thứ tự
console.log("Peak song song:", peak); // 2 — không bao giờ vượt limit
}
main();peak luôn dừng ở 2 dù có 6 tác vụ: pLimit chỉ cho active tối đa bằng limit, và mỗi khi một tác vụ xong thì next() mới kéo cái kế tiếp vào. Bạn vừa được tốc độ song song, vừa không quá tải server.
8. Hai helper hiện đại: withResolvers và try
Ngoài bốn combinator, ES gần đây thêm hai static method đáng biết.
8.1. Promise.withResolvers() (ES2024)
Trước đây, muốn resolve một promise từ bên ngoài executor (ví dụ khi một event tới), bạn phải “rò rỉ” hàm resolve ra ngoài bằng let:
1
2
3
4
5
6
// Kiểu cũ: khai báo let rồi gán trong executor — dài dòng
let resolveOld;
const oldPromise = new Promise((resolve) => {
resolveOld = resolve;
});
console.log("resolve đã lọt ra ngoài:", typeof resolveOld); // "function"Promise.withResolvers() trả thẳng về { promise, resolve, reject }, gọn hơn hẳn — hợp cho việc bọc một event / socket / callback thành promise:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Một "cổng" mở được từ bên ngoài: chờ event, socket, hay callback
function createDeferred() {
const { promise, resolve, reject } = Promise.withResolvers();
return { promise, open: resolve, fail: reject };
}
async function main() {
const gate = createDeferred();
// Ở nơi khác trong code, khi sự kiện tới thì mở cổng:
setTimeout(() => gate.open("socket connected"), 100);
const result = await gate.promise;
console.log(result); // "socket connected"
}
main();Promise.withResolvers() là Baseline từ 2024 (Chrome 119+, Firefox 121+, Safari 17.4+, Node 22+), dùng thoải mái trên runtime hiện đại.
8.2. Promise.try() (ES2025)
Promise.try(fn) chạy fn và bọc kết quả vào một promise — kể cả khi fn throw ngay lập tức (đồng bộ). Vấn đề nó giải: một hàm có thể vừa throw sync (ví dụ JSON.parse gặp input rác) vừa reject async, và bạn muốn gom cả hai loại lỗi vào cùng một .catch thay vì phải try/catch lồng trong .then.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function parseConfig(raw) {
return JSON.parse(raw); // throw ngay nếu raw không hợp lệ (đồng bộ)
}
async function main() {
// Cả lỗi sync (JSON.parse throw) lẫn async đều rơi vào cùng .catch
const ok = await Promise.try(() => parseConfig('{"port":3000}'))
.then((cfg) => `Port: ${cfg.port}`)
.catch((err) => `Lỗi parse: ${err.message}`);
console.log(ok); // "Port: 3000"
const bad = await Promise.try(() => parseConfig("{ khong-hop-le"))
.then((cfg) => `Port: ${cfg.port}`)
.catch((err) => `Lỗi parse: ${err.message}`);
console.log(bad); // "Lỗi parse: <message từ JSON.parse>"
}
main();Promise.try() mới hơn (ES2025), đã có trên các trình duyệt và runtime cập nhật gần đây (Chrome 128+, Firefox 134+, Safari 18.2+). Nếu phải chạy trên môi trường cũ, kiểm tra support hoặc dùng cách thủ công new Promise((resolve) => resolve(fn())) thay thế.
9. Vài cái bẫy còn lại
9.1. Fire-and-forget và unhandled rejection
Gọi một promise mà không await, không .catch — nếu nó reject, bạn có một unhandled rejection. Trên Node điều này có thể làm crash process; trên trình duyệt nó bắn ra sự kiện unhandledrejection. Quy tắc: promise chạy nền cũng phải có .catch.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function failAfter(ms, reason) {
return new Promise((_, reject) => setTimeout(() => reject(new Error(reason)), ms));
}
async function main() {
// ❌ Không .catch → unhandled rejection:
// failAfter(100, "gửi log thất bại");
// ✅ Gắn .catch cho tác vụ chạy nền
failAfter(100, "gửi log thất bại").catch((err) => {
console.log("Đã xử lý lỗi nền:", err.message);
});
await new Promise((r) => setTimeout(r, 200));
console.log("Main chạy xong bình thường, không bị crash");
}
main();9.2. .then(f1, f2) khác .then(f1).catch(f2)
Cả hai trông giống nhau nhưng bắt lỗi khác nhau. Trong .then(onFulfilled, onRejected), onRejected không bắt được lỗi do chính onFulfilled ném ra — nó chỉ xử lý reject từ phía trên. Còn .catch đứng sau .then thì bắt lỗi của cả hai.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async function main() {
// onRejected KHÔNG thấy lỗi do onFulfilled ném → phải .catch mới bắt được
await Promise.resolve("ok")
.then(
() => { throw new Error("lỗi trong onFulfilled"); },
(err) => console.log("A bắt được:", err.message) // KHÔNG chạy
)
.catch((err) => console.log("Chỉ .catch thấy:", err.message));
// .then(...).catch(...) bắt được lỗi của onFulfilled
await Promise.resolve("ok")
.then(() => { throw new Error("lỗi trong onFulfilled"); })
.catch((err) => console.log("B bắt được:", err.message));
}
main();Dòng A bắt được không bao giờ in ra — bằng chứng là onRejected trong cùng một .then không cứu được lỗi của onFulfilled. Vì vậy .catch đứng cuối chain là cách bắt lỗi an toàn hơn, trừ khi bạn cố ý muốn tách xử lý reject-của-upstream khỏi lỗi-của-onFulfilled.
Tóm lại, bốn combinator không thay thế nhau mà chia theo nhu cầu: cần đủ thì all, cần từng cái thì allSettled, đua tốc độ thì race, đua thành công thì any. Nhớ thêm ba điều: chạy song song thay vì await tuần tự, giới hạn concurrency khi số lượng lớn, và luôn có chỗ bắt lỗi cho promise chạy nền. Lần tới trước khi gõ Promise.all, thử hỏi lại “một cái fail thì mình có thật sự muốn mất hết không?” — thường câu trả lời sẽ chỉ bạn tới đúng combinator.