Bạn thêm async vào callback của forEach rồi await cả lời gọi, nhưng đoạn code phía sau vẫn chạy trước khi công việc trong callback xong. Vấn đề không nằm ở việc máy chạy quá nhanh. forEach không gom những Promise callback trả về thành một Promise đại diện cho cả lượt xử lý.
Bài đối chiếu JavaScript documentation ngày 30/10/2026 và chạy ví dụ bằng Node.js 24.18.1. Fixture dùng một gate Promise do chính code mở, không dùng mạng hoặc suy đoán thứ tự dựa vào số mili giây của timer. Kết quả chỉ chứng minh cơ chế chờ, không đo khả năng xử lý song song của hệ thống thật.
Đọc giá trị forEach thực sự trả về
Array.forEach reference ghi rằng callback được mong đợi là đồng bộ và forEach không chờ Promise. Async function vẫn tạo Promise, nhưng API gọi callback có sử dụng Promise đó hay không là một câu hỏi riêng.
Đoạn dưới giữ các callback ở gate để ta quan sát được điểm forEach đã kết thúc mà không dựa vào tốc độ thiết bị:
async function showForEach() {
const events = [];
let release;
const gate = new Promise(resolve => { release = resolve; });
const pending = [];
const returned = [1, 2].forEach(async id => {
const task = (async () => {
await gate;
events.push(`done:${id}`);
})();
pending.push(task);
await task;
});
await returned;
events.push('after-forEach');
console.log(returned === undefined);
console.log(events.join(','));
release();
await Promise.all(pending);
console.log(events.join(','));
}
showForEach().catch(console.error);
Kết quả là true, after-forEach, rồi after-forEach,done:1,done:2. Phần Promise.all trong fixture chỉ để dọn và chờ các task đã lưu sau khi mở gate; nó không phải hành vi forEach tự cung cấp.
await returned đang await undefined nên không tạo liên kết chờ với task. Callback async vẫn có Promise riêng, nhưng giá trị trả về của callback bị forEach bỏ qua. Async function reference giúp phân biệt Promise của function với cách bên gọi sử dụng nó.
Cần tuần tự thì await trong vòng lặp
Giả sử kết quả sau phụ thuộc bước trước hoặc bạn muốn dừng trước khi bắt đầu phần tử kế tiếp nếu có lỗi:
async function runSequential(items, save) {
const results = [];
for (const item of items) {
results.push(await save(item));
}
return results;
}
runSequential([1, 2], async id => `saved:${id}`)
.then(values => console.log(values.join(',')));
Ví dụ in saved:1,saved:2. Mỗi lời gọi save tiếp theo chỉ bắt đầu sau khi lời gọi trước hoàn tất. Khi save reject, hàm reject và vòng lặp không bắt đầu các phần tử còn lại, trừ khi bạn thêm cơ chế xử lý lỗi riêng.
for…of reference mô tả vòng lặp trên iterable. Điểm quan trọng trong code này là await nằm ngay trong thân vòng lặp của async function, không bị giấu trong callback của một API không chờ nó.
Công việc độc lập thì gom Promise có chủ đích
async function runTogether(items, save) {
return Promise.all(items.map(item => save(item)));
}
runTogether([1, 2], async id => `saved:${id}`)
.then(values => console.log(values.join(',')));
Code cũng trả saved:1,saved:2, nhưng hợp đồng bắt đầu công việc khác: map gọi save cho các phần tử để tạo danh sách Promise trước khi chờ cả nhóm. Thứ tự mảng kết quả tương ứng input không chứng minh các tác vụ hoàn tất theo thứ tự đó.
Không dùng map trên một danh sách khổng lồ rồi coi đây là bộ giới hạn concurrency. Nếu API có hạn mức hoặc tác vụ tốn tài nguyên, cần một cơ chế giới hạn riêng. Promise.all cũng không tự rollback những tác vụ đã ghi thành công.
Đặt kiểm tra lỗi ở đúng nơi
Một try/catch quanh await items.forEach(async ...) không tự bắt rejection xảy ra sau await bên trong callback. Để code gọi nhận được lỗi, phải return hoặc await một Promise thực sự đại diện cho công việc. Đừng chạy ca rejection không được quản lý chỉ để thử trên ứng dụng đang chạy.
Với runSequential, có thể dùng một save giả reject ở ID 2 và ghi lại ID đã bắt đầu để kiểm tra phần tử 3 chưa chạy. Với runTogether, quản lý các Promise theo hợp đồng của nhóm và xác định cần dừng phản hồi, thu đủ lỗi hay phục hồi phần nào. Bài không gọi API thật hoặc tạo side effect bên ngoài.
Lỗi thường gặp khi giải thích
- Nói async bị forEach “vô hiệu hóa”; callback vẫn async, chỉ có bên gọi không chờ.
- Thêm await trước forEach mà không đọc giá trị trả về.
- Dựa vào setTimeout để đoán mọi thứ tự thực thi thay vì kiểm soát fixture.
- Chọn chạy đồng thời chỉ vì cú pháp ngắn, bỏ qua dependency giữa các tác vụ.
Khi review, hỏi Promise nào đại diện cho toàn bộ công việc và ai đang await Promise đó. Câu hỏi này giúp chọn vòng lặp đúng, xử lý lỗi đúng và giải thích được vì sao code phía sau được phép tiếp tục.

