Async/await trong Node.js: các mẫu viết bất đồng bộ nên biết

  • author-image

    Nguyễn Đình Cường

  • blog-tag
  • blog-comment 0 comment
  • created-date 09 Nov, 2026
blog-thumbnail

Hầu như mọi thao tác trong backend Node.js đều bất đồng bộ: gọi database, đọc file, gọi API. Async/await giúp code đọc như tuần tự nhưng vẫn không chặn luồng. Tuy nhiên dùng sai cách, bạn sẽ gặp lỗi khó thấy hoặc làm chương trình chậm không cần thiết.

Nhắc nhanh: Promise và async/await

Một hàm async luôn trả về Promise. Từ khóa await tạm dừng hàm đó cho đến khi Promise xong, nhưng không chặn cả chương trình. Nó chỉ dùng được trong hàm async (hoặc ở cấp cao nhất của module ES).

async function getUserWithPosts(id) {  const user = await db.users.findById(id);  const posts = await db.posts.findByUser(id);  return { ...user, posts };}

Mẫu 1: chạy song song bằng Promise.all

Trong ví dụ trên, hai truy vấn độc lập nhưng bị chạy nối tiếp, tổng thời gian bằng tổng hai truy vấn. Nếu chúng không phụ thuộc nhau, hãy chạy song song:

async function getUserWithPosts(id) {  const [user, posts] = await Promise.all([    db.users.findById(id),    db.posts.findByUser(id),  ]);  return { ...user, posts };}

Lưu ý Promise.all thất bại ngay khi một Promise bị từ chối. Nếu muốn lấy kết quả của tất cả dù có cái lỗi, dùng Promise.allSettled.

Mẫu 2: await trong vòng lặp

Một lỗi hiệu năng thường gặp là await từng phần tử trong for khi các việc độc lập:

// Chậm: lần lượt từng cáifor (const id of ids) {  await sendEmail(id);}// Nhanh hơn: song songawait Promise.all(ids.map((id) => sendEmail(id)));

Nhưng chạy song song hàng nghìn việc cùng lúc có thể làm quá tải database hoặc API bên ngoài. Khi đó hãy giới hạn mức đồng thời, ví dụ chia mảng thành từng lô nhỏ hoặc dùng thư viện như p-limit. Còn khi thứ tự quan trọng hoặc mỗi bước phụ thuộc bước trước, vòng lặp tuần tự lại là lựa chọn đúng.

Mẫu 3: xử lý lỗi với try/catch

async function loadConfig() {  try {    const raw = await fs.promises.readFile('config.json', 'utf8');    return JSON.parse(raw);  } catch (err) {    if (err.code === 'ENOENT') return {}; // file chưa có thì dùng mặc định    throw err; // lỗi khác thì ném tiếp  }}

Chỉ bắt lỗi mà bạn thật sự xử lý được. Nuốt lỗi bằng catch rỗng là cách nhanh nhất để tạo ra bug bí ẩn. Với lỗi bạn không biết xử lý, hãy để nó nổi lên đến tầng có thể ghi log và phản hồi đúng cách.

Mẫu 4: timeout và hủy

Một lời gọi mạng treo có thể giữ tài nguyên mãi. Hãy đặt giới hạn thời gian:

const res = await fetch(url, { signal: AbortSignal.timeout(5000) });if (!res.ok) throw new Error('HTTP ' + res.status);const data = await res.json();

Những lỗi hay gặp

  • Quên await: bạn nhận về Promise thay vì giá trị, hoặc lỗi bị bỏ lơ.
  • Promise bị bỏ lơ (floating promise): lời gọi async không được await hay bắt lỗi sẽ gây unhandled rejection, có thể làm tiến trình thoát.
  • Dùng forEach với async: forEach không chờ các callback async; hãy dùng for...of hoặc Promise.all(map(...)).
  • Hàm async không cần thiết: thêm async vào hàm không có await chỉ thêm một lớp Promise vô ích.

Ví dụ tổng hợp: lấy dữ liệu có thử lại

Ghép các mẫu trên, bạn có thể viết một hàm lấy dữ liệu từ dịch vụ ngoài, có thời hạn chờ và thử lại vài lần khi lỗi tạm thời:

async function fetchWithRetry(url, retries = 3) {  for (let attempt = 1; attempt <= retries; attempt++) {    try {      const res = await fetch(url, { signal: AbortSignal.timeout(5000) });      if (!res.ok) throw new Error('HTTP ' + res.status);      return await res.json();    } catch (err) {      if (attempt === retries) throw err;      await new Promise((r) => setTimeout(r, attempt * 500)); // chờ lâu dần    }  }}

Hàm này tăng dần thời gian chờ giữa các lần thử, một kỹ thuật gọi là backoff, giúp không dồn dập gọi dịch vụ đang gặp sự cố. Hãy chỉ thử lại với lỗi tạm thời (mạng, 5xx), không thử lại với lỗi do chính request sai như 400 hay 404.

Gợi ý thực hành

Hãy lấy một đoạn code trong dự án của bạn có nhiều lời gọi await liên tiếp, vẽ ra sơ đồ phụ thuộc giữa chúng và xem chỗ nào có thể chạy song song. Đo thời gian trước và sau khi đổi sang Promise.all. Con số cụ thể sẽ thuyết phục bạn hơn mọi lý thuyết, và bạn sẽ có thói quen tự hỏi "việc này có thực sự phải chờ việc kia không?" mỗi khi viết một dòng await.

Kết luận

Async/await làm code bất đồng bộ dễ đọc, nhưng bạn vẫn phải nghĩ đến thứ tự, song song và lỗi. Hãy rà lại code của mình: chỗ nào đang await nối tiếp những việc độc lập, chỗ nào đang nuốt lỗi? Sửa hai điểm này thường đã cải thiện đáng kể cả tốc độ lẫn độ tin cậy.

Nếu muốn luyện tập cùng bài tập thực tế, hãy ghé các khóa học tại codebem.com khi bạn sẵn sàng.

author_photo
Nguyễn Đình Cường

0 comment