DEV Community

Async — Async Await

async/await: viết async đọc như đồng bộ mà không vô tình làm chậm

async/await là lớp cú pháp đặt trên Promise: một async function luôn trả về Promise, và await tạm dừng việc thực thi bên trong hàm đó cho tới khi Promise được chờ settle, rồi tiếp tục với giá trị resolve. Code async đọc tuần tự như code đồng bộ, try/catch bắt lỗi async tự nhiên. Nhưng chính sự "tuần tự" dễ đọc đó là cái bẫy hiệu năng lớn nhất: await liên tiếp các việc độc lập biến song song thành tuần tự, làm một API lẽ ra 200ms thành 2 giây.

Cơ chế hoạt động

await không block thread — nó nhường quyền điều khiển lại cho event loop, để các việc khác chạy, và lên lịch phần còn lại của hàm chạy tiếp (qua microtask) khi Promise settle. Hàm async bị "tạm dừng" tại await, nhưng process không hề đứng yên.

async function loadDashboard(userId) {
  const user = await fetchUser(userId)     // dừng tại đây, nhường event loop
  const orders = await fetchOrders(userId) // chạy tiếp khi user xong
  return { user, orders }
}
Enter fullscreen mode Exit fullscreen mode

try/catch quanh await bắt được rejection như lỗi đồng bộ — đây là lợi thế lớn về tính dễ đọc so với chuỗi .then/.catch.

Vấn đề gặp trong production

Failure mode: tuần tự hóa việc độc lập. Trong loadDashboard ở trên, fetchUserfetchOrders không phụ thuộc nhau — fetchOrders chỉ cần userId, không cần kết quả của fetchUser. Nhưng viết hai await liên tiếp buộc chúng chạy nối đuôi: tổng thời gian là tổng hai việc. Đây là nguyên nhân cực phổ biến của API chậm mà nhìn code thấy "có vẻ ổn":

// CHẬM: 200ms + 300ms = 500ms, dù hai việc độc lập
const user = await fetchUser(userId)
const orders = await fetchOrders(userId)

// NHANH: khởi động cả hai rồi mới chờ -> max(200,300) = 300ms
const [user, orders] = await Promise.all([fetchUser(userId), fetchOrders(userId)])
Enter fullscreen mode Exit fullscreen mode

Quy tắc: chỉ await tuần tự khi bước sau thật sự cần kết quả bước trước. Việc độc lập phải khởi động cùng lúc rồi gom bằng Promise.all. Một vòng lặp for ... await gọi API cho từng phần tử là dạng tệ nhất của lỗi này — N việc độc lập chạy nối đuôi:

// CHẬM: N lần round-trip nối đuôi
for (const id of ids) results.push(await fetchUser(id))

// NHANH: song song (có kiểm soát concurrency để không quá tải)
const results = await Promise.all(ids.map(fetchUser))
Enter fullscreen mode Exit fullscreen mode

Khi nào KHÔNG nên song song hết: nếu ids có hàng nghìn phần tử, Promise.all bắn hết một lúc sẽ làm quá tải DB/API đích hoặc cạn connection pool. Lúc đó cần giới hạn concurrency (xử lý theo lô, hoặc dùng một pool giới hạn N việc đồng thời) — song song có kiểm soát, không phải tất cả hoặc không gì.

Cách debug và monitor

Khi một endpoint chậm, đo thời gian từng await (log timestamp trước/sau, hoặc dùng tracing) và tìm các await liên tiếp trên việc độc lập — đó là điểm gộp được vào Promise.all. Một dấu hiệu rõ trong code: nhiều dòng await liền nhau mà biến của dòng sau không dùng kết quả dòng trước. Với for...await gọi I/O, gần như luôn nên hỏi "việc này có cần tuần tự không"; nếu không, đổi sang song song có giới hạn. Để tránh quá tải, theo dõi số kết nối đang mở tới DB và độ trễ của API đích khi tăng concurrency — tăng song song quá mức làm chậm ngược hoặc gây timeout.

Tradeoff

await tuần tự dễ đọc, dễ suy luận, và bắt buộc khi có phụ thuộc dữ liệu giữa các bước — nhưng cộng dồn độ trễ. Promise.all song song nhanh hơn nhiều cho việc độc lập nhưng khó đọc hơn chút và dễ làm quá tải hệ thống đích nếu không giới hạn concurrency. Quy tắc thực tế: tuần tự khi có phụ thuộc thật; song song cho việc độc lập; song song có giới hạn khi số lượng lớn. Tối ưu sai hướng (song song hóa thứ có phụ thuộc) gây bug dữ liệu; không tối ưu (tuần tự hóa thứ độc lập) gây chậm — cả hai đều phải tránh bằng cách nhìn đúng quan hệ phụ thuộc.

Câu hỏi phỏng vấn

Khi nào không nên dùng await tuần tự, và await có block thread không?

await không block thread — nó nhường quyền cho event loop để việc khác chạy, rồi tiếp tục hàm qua microtask khi Promise settle, nên process vẫn xử lý request khác trong lúc một hàm đang chờ. Không nên await tuần tự khi các việc độc lập với nhau (bước sau không cần kết quả bước trước): hai await liên tiếp hay một for...await trên việc độc lập cộng dồn độ trễ thành tổng, trong khi gom vào Promise.all chạy song song chỉ tốn bằng việc chậm nhất. Điểm ăn điểm là nêu giới hạn: với số lượng lớn, Promise.all không giới hạn làm quá tải DB/API hoặc cạn connection pool, nên cần song song có kiểm soát concurrency (theo lô hoặc pool giới hạn).

Hands-on

Lấy một endpoint thật cần nhiều nguồn dữ liệu (ví dụ trang dashboard gọi user, orders, notifications) và viết phiên bản await tuần tự, đo tổng thời gian. Xác định các lệnh gọi độc lập và gom vào Promise.all, đo lại để thấy thời gian giảm xuống còn việc chậm nhất. Sau đó dựng một tác vụ xử lý hàng nghìn id bằng Promise.all không giới hạn để quan sát connection pool cạn hoặc API đích timeout, rồi áp một bộ giới hạn concurrency (xử lý theo lô N hoặc dùng một thư viện pool) và so sánh độ ổn định lẫn thông lượng giữa ba cách.

Top comments (1)

Collapse
 
frank_signorini profile image
Frank

Great explanation of how async/await helps