Đừng Dùng Promise.race Cho Fallback Nữa: Hãy Dùng Promise.any và AggregateError

Table of Contents
Promise.any
Promise.any
AggregateError
AggregateError
Promise.all
Promise.all
Promise.allSettled
Promise.allSettled
Promise.race
Promise.race
Promise.any và AggregateError: Mô hình bạn không biết mình cần
Tôi đã bỏ qua Promise.any trong nhiều tháng vì tôi nghĩ nó chỉ là mặt đối lập của Promise.all — thú vị nhưng ngách. Tôi đã sai. Đó là giải pháp sạch nhất cho một loại vấn đề thực tế mà trước đây đòi hỏi những cách giải quyết khó xử.
Bức tranh tổng thể về Combinator
JavaScript cung cấp cho chúng ta bốn Promise combinator. Chúng hoạt động như một gia đình. Hiểu chúng cùng nhau nhanh hơn là học chúng riêng lẻ:
| Combinator | Giải quyết khi | Từ chối khi |
|---|---|---|
Promise.all | Mọi promise đều giải quyết | Bất kỳ promise nào từ chối |
Promise.allSettled | Mọi promise đều được giải quyết (luôn luôn) | Không bao giờ — luôn giải quyết |
Promise.race | Bất kỳ promise nào được giải quyết | Bất kỳ promise nào từ chối |
Promise.any | Bất kỳ promise nào giải quyết | Mọi promise đều từ chối |
Promise.any nằm ở góc phần tư trống: giải quyết khi thành công đầu tiên và tổng hợp — không phải đoản mạch — các lỗi. Đó là toàn bộ ý nghĩa của nó.
Mẹo ghi nhớ
any + settled hoạt động tương tự như race + all: any = thắng đầu tiên, settled = chờ đợi mọi người. Bạn có thể suy luận về chúng bằng cách kết hợp hai trục đó thay vì ghi nhớ bốn tên.
Cách tiếp cận sai lầm mà nó thay thế
Đây là những gì tôi thường viết bất cứ khi nào tôi cần hành vi "dự phòng":
| Mô hình cũ | Những gì bạn thực sự cần | |
|---|---|---|
| thử A, nếu thất bại thử B, nếu thất bại thử C | Giải quyết khi thành công đầu tiên — không chờ đợi tuần tự, không lãng phí băng thông | Sử dụng Promise.any cho thành công đầu tiên |
| kiểm tra fetch nào đã giải quyết, bỏ qua phần còn lại | AggregateError cung cấp cho bạn chính xác chi tiết đó một cách miễn phí | Bạn sẽ cần tự kiểm tra promise nào đã được giải quyết trước |
| `Promise.race` cho timeout hoặc fallback | `Promise.any` — thành công đầu tiên, báo cáo lỗi trung thực | Race thắng khi giải quyết, không phải thành công — âm thầm bỏ qua lỗi |
Cách tiếp cận cũ: gọi dịch vụ nhanh nhất, sử dụng kết quả của nó nếu tốt, dự phòng nếu không. Promise.race có vẻ hấp dẫn nhưng lại thất bại âm thầm trong cuộc đua — bạn không biết liệu người thắng cuộc có thành công hay chỉ phản hồi trước. Promise.any giải quyết trực tiếp vấn đề này.
Cách sử dụng cơ bản
Ví dụ tối thiểu
const urls = [
"https://api.primary.com/data",
"https://api.backup-1.com/data",
"https://api.backup-2.com/data",
];
try {
const response = await Promise.any(
urls.map((url) => fetch(url).then((r) => r.json()))
);
console.log(
"Got data from first successful fetch:",
response
);
} catch (aggregateError) {
// All three failed — see exactly why
console.log(
"All endpoints failed:",
aggregateError.errors
);
aggregateError.errors.forEach((err, i) => {
console.log(`Source ${i}:`, err.message);
});
}
Chi tiết quan trọng: khi Promise.any từ chối, lỗi là một AggregateError, không phải một Error đơn thuần. Nó mang một mảng .errors với một mục nhập cho mỗi đầu vào bị lỗi, theo cùng thứ tự với iterable đầu vào.
Các trường hợp sử dụng không rõ ràng
1. Fetch cờ tính năng song song với dự phòng. Bạn gọi ba nhà cung cấp cờ đồng thời. Bạn muốn kết quả từ bất kỳ nhà cung cấp nào phản hồi trước — và chỉ quan tâm đến lỗi nếu mọi nhà cung cấp đều ngừng hoạt động.
2. Wrapper timeout không có ngữ nghĩa race. Bọc một cuộc gọi chậm trong một timeout:
async function fetchWithTimeout(url, ms) {
return Promise.any([
fetch(url),
new Promise((_, reject) =>
setTimeout(
() => reject(new Error("Timeout")),
ms
)
),
]);
}
Điều này sạch hơn Promise.race vì timeout chỉ ăn reject, không phải resolve.
3. Tải hình ảnh lũy tiến. Giải quyết trên định dạng đầu tiên mà trình duyệt hỗ trợ:
const sources = [
fetchImage("webp"),
fetchImage("avif"),
fetchImage("png"), // fallback
];
const image = await Promise.any(sources); // Use whichever format loaded first
4. Làm mới token xác thực linh hoạt. Làm mới từ hai nhà cung cấp đồng thời — sử dụng bất kỳ cái nào thành công:
const tokens = await Promise.any([
refreshGitHubToken(),
refreshGitLabToken(),
]); // Use tokens regardless of which provider responded
AggregateError chuyên sâu
Đối tượng AggregateError tiết lộ nhiều hơn một sự từ chối đơn thuần:
try {
await Promise.any(failingPromises);
} catch (err) {
// err is an AggregateError
console.log(err.name); // "AggregateError"
console.log(err.message); // "All promises were rejected"
console.log(err.errors); // [TypeError, ReferenceError, RangeError]
...
}
err.errors giữ lại các đối tượng lỗi riêng lẻ để bạn có thể phân nhánh logic theo từng nguồn. Đây là điều làm cho Promise.any thực sự hữu ích cho việc ghi nhật ký và báo cáo lỗi — không chỉ là ngữ nghĩa "thành công đầu tiên".
Đòn bẩy song song
Giảm độ trễ bằng cách chạy I/O song song là thủ thuật tối ưu hóa JavaScript lâu đời nhất. Promise.all đã đề cập đến trường hợp "tất cả phải thành công". Promise.any đề cập đến "ít nhất một thành công" — đây là hầu hết các yêu cầu trong thế giới thực.
So sánh:
- Một lệnh gọi API duy nhất thử lại khi thất bại là chi phí tuần tự — bạn chờ timeout cho mỗi lần thử.
- Cùng một yêu cầu được gửi đến hai dịch vụ song song với
Promise.anyđược giới hạn thời gian bởi cái tốt nhất trong cả hai — chi phí thất bại chậm được giới hạn ở timeout nhanh hơn.
Sự bất đối xứng này quan trọng nhất trên thiết bị di động hoặc kết nối có độ trễ cao, nơi các timeout riêng lẻ dài.
Khả năng tương thích và Lưu ý
- Yêu cầu Node.js 15+, Deno 1.0+, hoặc tất cả các trình duyệt hiện đại (Chrome 85+, Firefox 79+, Safari 14+). Không có sẵn trong Node 14.
- Khi mọi đầu vào đều từ chối,
AggregateError.messagekết quả luôn là chuỗi ký tự"All promises were rejected"— không phân tích cú pháp nó để gỡ lỗi, hãy sử dụng.errorsthay thế. - Các mục iterable không phải Promise được ép kiểu chính xác như các phương thức Promise khác (hành vi nhất quán kể từ ES2020).
- Bởi vì
Promise.anychỉ ngừng gọi.thenkhi giải quyết, một đầu vào từ chối vẫn từ chối ngay cả sau khi một đầu vào khác đã giải quyết. Các từ chối bổ sung tạo ra cảnh báo từ chối không được xử lý trừ khi được bắt. - Một iterable rỗng từ chối ngay lập tức:
Promise.any([])từ chối vớiAggregateErrorcó mảng.errorstrống. Điều này khác vớiPromise.all([])giải quyết với[].
Tham khảo nhanh
// Basic: first success wins
const winner = await Promise.any([
fetch(urlA),
fetch(urlB),
fetch(urlC),
]);
// Fallback: value from whichever call resolved first
// All fail: catch AggregateError — inspect .errors
// Timeout pattern
async function withTimeout(promise, ms) {
return Promise.any([
promise,
new Promise((_, reject) =>
setTimeout(
() => reject(
new Error(`Timed out after ${ms}ms`)
),
ms
)
),
]);
}
// Typed (TypeScript)
function firstSuccessful<T>(
promises: Array<Promise<T>>
): Promise<T> {
return Promise.any(promises);
}
Bạn cũng có thể thích
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

Làm chủ kiểm thử E2E với Playwright vào năm 2026
Làm chủ kiểm thử E2E với Playwright vào năm 2026: tự động chờ, cách ly ngữ cảnh trình duyệt, chặn mạng, lưu trữ xác thực và song song hóa CI.
Read more
Các Mẫu TypeScript Nâng Cao cho Ứng Dụng Doanh Nghiệp
Nắm vững các mẫu TypeScript doanh nghiệp nâng cao: branded types, conditional response types, template literal routing và toán tử satisfies.
Read more
Quản lý trạng thái trong React 2026: Vượt xa Redux
Hướng dẫn toàn diện về quản lý trạng thái React năm 2026: so sánh React 19 actions, trạng thái máy chủ TanStack Query, Zustand, Jotai và Signals.
Read more