Đang tải...

Ếch Trendy

Cùng chú ếch nhỏ khám phá thế giới Web đầy biến động. Cập nhật trending nhanh như cách ếch đớp mồi! ⌨️🌿


0

JavaScript bất đồng bộ: vì sao forEach không chờ await?

Ếch Trendy
Ếch Trendy

6 giờ tới · 9 phút đọc

Vấn đề: log kết quả trước khi request xong

Bạn viết một vòng lặp tưởng chừng đơn giản:

// Giả định minh họa - KHÔNG chạy đúng như mong đợi
const ids = [1, 2, 3];

ids.forEach(async (id) => {
  const data = await fetchUser(id); // giả định fetchUser trả về Promise
  console.log(data);
});

console.log('Xong rồi!');

Kết quả in ra terminal:

Xong rồi!
{ id: 1, name: 'Alice' }
{ id: 2, name: 'Bob' }
{ id: 3, name: 'Carol' }

'Xong rồi!' xuất hiện trước cả ba kết quả. Không phải bug ngẫu nhiên - đây là hành vi đúng theo spec của JavaScript.

348202 forEach không chờ callback async - đây là thiết kế, không phải bug

Tại sao lại thế? forEach được thiết kế cho callback đồng bộ. Khi bạn truyền async function vào, nó vẫn gọi callback và nhận lại một Promise - nhưng không await Promise đó. Vòng lặp tiếp tục chạy ngay lập tức mà không chờ Promise resolve.

Nói cách khác: forEach không quan tâm callback trả về gì. Nó gọi xong là bỏ qua.


Tái hiện lỗi để hiểu rõ hơn

Muốn quan sát rõ hành vi này, bạn có thể tự mô phỏng fetchUser bằng một hàm trả về Promise sau một khoảng delay (đây là bài tập giả định để quan sát thứ tự):

// Hàm giả lập API call - bài tập minh họa
function fakeApi(id, delay = 500) {
  return new Promise((resolve) => {
    setTimeout(() => resolve({ id, name: `User ${id}` }), delay);
  });
}

const ids = [1, 2, 3];

console.log('Bắt đầu forEach...');

ids.forEach(async (id) => {
  const data = await fakeApi(id);
  console.log('Nhận được:', data);
});

console.log('forEach kết thúc - nhưng data chưa về!');

Chạy đoạn trên trong Node.js hoặc browser console, bạn thấy ngay:

Bắt đầu forEach...
forEach kết thúc - nhưng data chưa về!
Nhận được: { id: 1, name: 'User 1' }
Nhận được: { id: 2, name: 'User 2' }
Nhận được: { id: 3, name: 'User 3' }

348203 Chạy thử trong Node.js để thấy thứ tự log - kết quả không sai, chỉ khác kỳ vọng

Thứ tự hoàn tất của id 1, 2, 3 có thể thay đổi nếu bạn truyền delay khác nhau cho từng call. Thử fakeApi(1, 800), fakeApi(2, 200), fakeApi(3, 500) - bạn sẽ thấy id 2 về trước. Đây là bằng chứng rõ nhất: forEach không đảm bảo thứ tự hoàn tất, không chờ bất kỳ call nào.


Ba cách xử lý: so sánh for...of, Promise.all và forEach

Ba cách tiếp cận giải quyết bài toán "lấy dữ liệu cho danh sách ID" - mỗi cách có trade-off khác nhau.

348204 Chọn đúng cách tùy theo yêu cầu tuần tự hay song song

Cách 1: for...of - tuần tự, chắc chắn

// Bài tập minh họa - chạy tuần tự
async function fetchSequential(ids) {
  const results = [];
  for (const id of ids) {
    const data = await fakeApi(id); // chờ xong mới sang id tiếp theo
    results.push(data);
    console.log('Tuần tự nhận:', data);
  }
  return results;
}

fetchSequential([1, 2, 3]).then((all) => {
  console.log('Tất cả kết quả:', all);
});

Kết quả đúng như mong đợi - id 1 xong mới sang id 2. Tổng thời gian = tổng delay của tất cả request. Phù hợp khi request sau phụ thuộc kết quả request trước (ví dụ: lấy user ID rồi mới lấy orders của user đó).

Cách 2: Promise.all - song song, nhanh hơn

// Bài tập minh họa - chạy song song
async function fetchParallel(ids) {
  const promises = ids.map((id) => fakeApi(id));
  const results = await Promise.all(promises);
  console.log('Song song, tất cả kết quả:', results);
  return results;
}

fetchParallel([1, 2, 3]);

Promise.all bắn tất cả request cùng lúc, chờ đến khi request cuối cùng hoàn tất mới trả về. Tổng thời gian = delay của request lâu nhất (không phải tổng). Phù hợp khi các request độc lập nhau.

Lưu ý quan trọng: nếu bất kỳ Promise nào reject, Promise.all sẽ reject ngay lập tức - các Promise còn lại vẫn chạy nhưng kết quả bị bỏ qua. Dùng Promise.allSettled nếu muốn lấy kết quả kể cả khi một số request thất bại.

Cách 3: forEach async - không kiểm soát được

// Cách NÀY không kiểm soát được thứ tự hoặc khi nào xong
ids.forEach(async (id) => {
  const data = await fakeApi(id);
  console.log(data);
});
// Không có cách await forEach này từ bên ngoài!

Không có cơ chế nào để biết khi nào tất cả callback trong forEach đã hoàn tất. Không thể await forEach(...) vì forEach luôn trả về undefined.


Bảng so sánh điều kiện áp dụng

Tình huống Nên dùng Lý do
Request sau phụ thuộc request trước for...of Đảm bảo thứ tự, có thể dùng kết quả trước
Các request độc lập, cần nhanh Promise.all Chạy song song, tổng thời gian = request chậm nhất
Muốn thu thập kết quả kể cả khi lỗi Promise.allSettled Không throw khi một request fail
Chỉ cần side-effect, không cần kết quả for...of hoặc Promise.all forEach async không đảm bảo gì
Giới hạn số request đồng thời (rate limit) for...of theo batch Tránh spam server

348205 Chọn sai phương pháp có thể làm server quá tải hoặc kết quả sai thứ tự

Promise.allSettled là lựa chọn khi bạn gọi nhiều API và chấp nhận một số request thất bại - nó trả về mảng object với status: 'fulfilled' hoặc status: 'rejected' cho từng item:

// Bài tập minh họa - xử lý lỗi từng request
async function fetchWithFallback(ids) {
  const promises = ids.map((id) => fakeApi(id));
  const results = await Promise.allSettled(promises);
  
  results.forEach((result, index) => {
    if (result.status === 'fulfilled') {
      console.log(`ID ${ids[index]}:`, result.value);
    } else {
      console.error(`ID ${ids[index]} lỗi:`, result.reason);
    }
  });
}

Lưu ý: forEach ở dòng cuối trong ví dụ này hợp lệ vì callback không dùng await - chỉ đọc kết quả đã có sẵn, không có async nào cần chờ.


Bài tập thực hành: ba cách lấy dữ liệu cho danh sách ID

Dưới đây là bài tập đầy đủ để bạn tự chạy và quan sát. Copy vào Node.js (file .js) hoặc browser console.

// === BÀI TẬP MINH HỌA - tự chạy để quan sát ===

// Hàm giả lập API với delay ngẫu nhiên và tỉ lệ lỗi
function fakeApi(id) {
  const delay = Math.floor(Math.random() * 800) + 200; // 200-1000ms
  const shouldFail = id === 99; // id=99 luôn lỗi để test
  
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (shouldFail) {
        reject(new Error(`Không tìm thấy user ${id}`));
      } else {
        resolve({ id, name: `User ${id}`, delay });
      }
    }, delay);
  });
}

const ids = [1, 2, 3, 4, 5];

// --- Cách 1: for...of (tuần tự) ---
async function cach1_tuanTu() {
  console.log('\n=== Cách 1: for...of tuần tự ===');
  const start = Date.now();
  const results = [];
  
  for (const id of ids) {
    try {
      const data = await fakeApi(id);
      results.push(data);
      console.log(`✓ ID ${id} xong sau ${data.delay}ms`);
    } catch (err) {
      console.error(`✗ ID ${id} lỗi:`, err.message);
    }
  }
  
  console.log(`Tổng thời gian: ${Date.now() - start}ms`);
  return results;
}

// --- Cách 2: Promise.all (song song) ---
async function cach2_songSong() {
  console.log('\n=== Cách 2: Promise.all song song ===');
  const start = Date.now();
  
  try {
    const results = await Promise.all(ids.map(fakeApi));
    console.log('Tất cả kết quả:', results.map(r => `ID ${r.id} (${r.delay}ms)`));
    console.log(`Tổng thời gian: ${Date.now() - start}ms`);
    return results;
  } catch (err) {
    console.error('Có request lỗi - Promise.all dừng:', err.message);
  }
}

// --- Cách 3: Promise.allSettled (song song, chịu lỗi) ---
async function cach3_allSettled() {
  console.log('\n=== Cách 3: Promise.allSettled ===');
  const start = Date.now();
  const idsWithBad = [...ids, 99]; // thêm id lỗi
  
  const results = await Promise.allSettled(idsWithBad.map(fakeApi));
  
  results.forEach((result, index) => {
    const id = idsWithBad[index];
    if (result.status === 'fulfilled') {
      console.log(`✓ ID ${id}:`, result.value.name);
    } else {
      console.error(`✗ ID ${id} lỗi:`, result.reason.message);
    }
  });
  
  console.log(`Tổng thời gian: ${Date.now() - start}ms`);
}

// Chạy tuần tự để quan sát từng cách
(async () => {
  await cach1_tuanTu();
  await cach2_songSong();
  await cach3_allSettled();
})();

348206 Chạy thử cả ba hàm rồi so sánh thời gian thực tế

Điều cần quan sát khi chạy:

  • Cách 1 (for...of): tổng thời gian ≈ tổng delay của cả 5 request cộng lại.
  • Cách 2 (Promise.all): tổng thời gian ≈ delay của request chậm nhất trong 5 request.
  • Cách 3 (Promise.allSettled): tương tự cách 2 về thời gian, nhưng không throw khi id 99 lỗi.

Nếu muốn hiểu thêm nền tảng async/await từ đầu, khóa Lập Trình JavaScript Cơ Bản trên F8 có phần bất đồng bộ khá chi tiết - miễn phí.


Lỗi thường gặp và cách tránh

Lỗi 1: dùng forEach cho async rồi await bên ngoài

// SAI - forEach trả về undefined, không thể await
await ids.forEach(async (id) => { ... }); // không có tác dụng gì

await undefined = không chờ gì cả. Không báo lỗi, chỉ im lặng bỏ qua.

Lỗi 2: quên try/catch trong for...of

// Nếu một request lỗi, vòng lặp dừng ngay - các ID sau không được xử lý
for (const id of ids) {
  const data = await fakeApi(id); // throw -> dừng for...of
  results.push(data);
}

Bọc try/catch bên trong for nếu muốn tiếp tục khi một request thất bại:

for (const id of ids) {
  try {
    const data = await fakeApi(id);
    results.push(data);
  } catch (err) {
    console.error(`ID ${id} thất bại:`, err.message);
    // tiếp tục sang id tiếp theo
  }
}

Lỗi 3: Promise.all với danh sách lớn

Gọi 500 request cùng lúc bằng Promise.all có thể làm quá tải server hoặc bị rate limit. Giải pháp: chia batch.

// Bài tập minh họa - xử lý theo batch
async function fetchInBatches(ids, batchSize = 5) {
  const results = [];
  for (let i = 0; i < ids.length; i += batchSize) {
    const batch = ids.slice(i, i + batchSize);
    const batchResults = await Promise.all(batch.map(fakeApi));
    results.push(...batchResults);
    console.log(`Batch ${i / batchSize + 1} xong`);
  }
  return results;
}

348207 Batch processing: cân bằng giữa tốc độ và tải server

Lỗi 4: nhầm map và forEach khi cần collect kết quả

forEach không trả về mảng kết quả. Nếu cần collect Promise, dùng .map() thay vì .forEach():

// Đúng: dùng map để tạo mảng Promise, rồi Promise.all
const promises = ids.map((id) => fakeApi(id));
const results = await Promise.all(promises);

// Sai: forEach không trả về gì
const promises2 = ids.forEach((id) => fakeApi(id)); // undefined!

Trang web dùng cookie để hoạt động và đo hiệu quả quảng cáo. Xem chính sách bảo mật