•9 min read

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

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

Promise.any

Click to reveal
Promise Combinators
Phương thức tĩnh nhận một iterable các Promise và trả về một Promise mới sẽ được giải quyết ngay khi BẤT KỲ đầu vào nào được giải quyết, chỉ bị từ chối nếu tất cả các đầu vào đều bị từ chối.

Promise.any

Promise Combinators

AggregateError

Click to reveal
Promise Combinators
Một lớp con Error mới được giới thiệu trong ES2021, đóng gói nhiều lỗi riêng lẻ vào một mảng lỗi duy nhất, được Promise.any sử dụng nội bộ.

AggregateError

Promise Combinators

Promise.all

Click to reveal
Promise Combinators
Phương thức tĩnh nhận một iterable các Promise và trả về một Promise mới chỉ được giải quyết khi TẤT CẢ các đầu vào được giải quyết, hoặc bị từ chối ngay lập tức nếu bất kỳ đầu vào nào bị từ chối.

Promise.all

Promise Combinators

Promise.allSettled

Click to reveal
Promise Combinators
Phương thức tĩnh trả về một Promise luôn được giải quyết sau khi tất cả các Promise đầu vào đã được giải quyết, cung cấp một mảng các đối tượng kết quả bất kể thành công hay thất bại.

Promise.allSettled

Promise Combinators

Promise.race

Click to reveal
Promise Combinators
Phương thức tĩnh nhận một iterable các Promise và trả về một Promise sẽ được giải quyết ngay khi BẤT KỲ đầu vào nào được giải quyết — cho dù nó được giải quyết hay bị từ chối.

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ử.

Audio Briefing
0:00 / 0:00

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ẻ:

CombinatorGiải quyết khiTừ chối khi
Promise.allMọi promise đều giải quyếtBất kỳ promise nào từ chối
Promise.allSettledMọi promise đều được giải quyết (luôn luôn)Không bao giờ — luôn giải quyết
Promise.raceBất kỳ promise nào được giải quyếtBất kỳ promise nào từ chối
Promise.anyBất kỳ promise nào giải quyếtMọ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.

Advertisement

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
Advertisement

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.message kế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 .errors thay 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.any chỉ ngừng gọi .then khi 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ới AggregateError có mảng .errors trống. Điều này khác với Promise.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

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement