Câu hỏi “một request trong Promise.all thất bại thì chuyện gì xảy ra?” thường nhận câu trả lời quá ngắn: tất cả dừng lại. Thực tế, promise tổng hợp có thể reject trong khi công việc khác vẫn tiếp tục. Nhầm lẫn này gây lỗi khi xử lý upload, lưu dữ liệu hoặc tải nhiều phần của một màn hình.
Bài này dùng các promise tự điều khiển để thấy rõ hành vi mà không phụ thuộc mạng hay thời gian chờ. Hai ví dụ đã chạy bằng Node.js 24.18.1 dưới dạng file .mjs. Tài liệu MDN được đối chiếu ngày 24/10/2026. Đây là kiểm tra ngữ nghĩa JavaScript, không phải đo hiệu năng request đồng thời.
1. Promise.all tạo một kết quả chung
Theo tham chiếu Promise.all, promise tổng hợp fulfill khi tất cả đầu vào fulfill, hoặc reject khi một đầu vào reject. Khi thành công, mảng kết quả giữ thứ tự đầu vào, không xếp theo tác vụ hoàn thành trước.
Điều này phù hợp khi bước sau cần toàn bộ dữ liệu. Ví dụ, bạn chỉ dựng một báo cáo hoàn chỉnh khi các phần bắt buộc đều có. Nhưng reject của kết quả chung không tự gửi tín hiệu hủy tới các công việc đã bắt đầu, và không hoàn tác tác dụng phụ đã xảy ra.
2. Chứng minh công việc còn lại vẫn tiếp tục
Lưu thành all-demo.mjs rồi chạy node all-demo.mjs:
let finishSlow;
const slow = new Promise((resolve) => {
finishSlow = resolve;
});
const batch = Promise.all([
Promise.reject(new Error('A failed')),
slow,
]);
try {
await batch;
} catch (error) {
console.log(error.message);
}
finishSlow('B done');
console.log(await slow);
Output đã kiểm tra là A failed, rồi B done, mỗi chuỗi trên một dòng. Promise slow vẫn có thể được resolve sau khi batch đã reject. Ta chủ động điều khiển thời điểm hoàn thành thay vì dùng hai timer rồi đoán tác vụ nào nhanh hơn.
Trong code gọi mạng, muốn hủy request phải dùng cơ chế hủy mà thao tác đó hỗ trợ, chẳng hạn một tín hiệu abort được truyền và được xử lý đúng. Promise combinator không cung cấp cơ chế hủy phổ quát cho mọi loại công việc. Ngay cả khi request client được hủy, cũng cần hiểu server có thể đã nhận và thực hiện công việc.
3. allSettled giữ từng kết quả để bạn quyết định
Promise.allSettled chờ mọi đầu vào kết thúc và trả các bản ghi có status. Bản ghi fulfilled có value, bản ghi rejected có reason. Mảng vẫn theo thứ tự đầu vào.
Lưu đoạn sau thành settled-demo.mjs:
let finishFirst;
const first = new Promise((resolve) => {
finishFirst = resolve;
});
const second = Promise.resolve('second');
const third = Promise.reject(new Error('third failed'));
const batch = Promise.allSettled([first, second, third]);
finishFirst('first');
const results = await batch;
for (const [index, result] of results.entries()) {
if (result.status === 'fulfilled') {
console.log(index, 'ok', result.value);
} else {
console.log(index, 'error', result.reason.message);
}
}
Kết quả đã chạy là 0 ok first, 1 ok second, 2 error third failed. Dù đầu vào thứ hai đã fulfilled trước đầu vào thứ nhất, vị trí kết quả không đổi. Việc gắn kết quả về đúng mục gốc vẫn cần ID hoặc index đáng tin cậy trong ứng dụng của bạn.
4. Chọn theo chính sách lỗi
Nếu màn hình cho phép một khối thông tin lỗi nhưng khối khác vẫn hiển thị, allSettled có thể giúp thu đủ tình trạng để dựng UI. Tuy nhiên, bạn vẫn phải đọc từng status và quyết định retry, thông báo hay bỏ qua theo nghiệp vụ. Chờ tất cả xong rồi không xử lý rejected chỉ làm lỗi bị che đi.
Nếu thiếu một phần bắt buộc thì toàn bộ bước sau không có ý nghĩa, all với catch ở ranh giới phù hợp thường mô tả ý định rõ hơn. Hãy hỏi thêm việc phụ nào vẫn được phép tiếp tục khi kết quả chung thất bại và bên nào có trách nhiệm hủy nó.
Cả hai không phải hàng đợi giới hạn số tác vụ. Với items.map(item => doWork(item)), các lời gọi doWork đã được thực hiện trong quá trình map. Đưa mảng đó vào allSettled không tự biến một nghìn công việc thành nhóm năm công việc mỗi lượt. Muốn giới hạn tải cần một thiết kế điều phối riêng.
Kiểm tra và lỗi thường gặp
Lỗi thứ nhất là await trực tiếp mảng do map(async ...) trả về rồi tưởng đã nhận mảng kết quả. Hãy xác định biến đang chứa promise hay mảng promise và dùng combinator theo ý định. Lỗi thứ hai là lấy mọi result.value từ allSettled mà không phân nhánh status.
Lỗi thứ ba là viết catch cho all rồi giả định không còn công việc nào có thể ghi dữ liệu. Khi có side effect, kiểm tra cả cơ chế hủy và hành vi phục hồi của tác vụ. Cuối cùng, đừng dùng for...of có await bên trong rồi gọi nó là chạy song song; mỗi lượt vòng lặp đang chờ lượt trước hoàn thành.
Gợi ý trả lời phỏng vấn
Hãy trình bày điều kiện hoàn thành, hình dạng kết quả, thứ tự và chính sách lỗi. Sau đó nêu rõ combinator không tự hủy tác vụ hay rollback dữ liệu. Hai ví dụ có thể chạy ở trên giúp bạn chứng minh câu trả lời thay vì chỉ nhắc lại tên “fail fast”.



