Phỏng vấn JavaScript bất đồng bộ: Promise, async/await và event loop

  • author-image

    Nguyễn Đình Cường

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

Bất đồng bộ là chủ đề gần như chắc chắn xuất hiện khi phỏng vấn JavaScript. Người phỏng vấn muốn biết bạn có hiểu vì sao đoạn code của mình in kết quả theo thứ tự lạ hay không. Bài này giúp bạn nắm event loop, Promise và async/await bằng ngôn ngữ dễ hiểu kèm ví dụ.

JavaScript đơn luồng nhưng vẫn xử lý bất đồng bộ

JavaScript chạy code trên một luồng duy nhất, nghĩa là mỗi lúc chỉ thực thi một việc. Các thao tác chậm như gọi mạng, đọc file hay hẹn giờ được giao cho môi trường (trình duyệt hoặc Node.js). Khi hoàn tất, callback của chúng được đưa vào hàng đợi, và event loop sẽ lấy ra chạy khi ngăn xếp gọi (call stack) trống.

Event loop: thứ tự thực thi

Có hai hàng đợi quan trọng:

  • Macrotask queue: chứa setTimeout, setInterval, sự kiện giao diện.
  • Microtask queue: chứa callback của Promise (then, catch, finally) và queueMicrotask.

Sau khi code đồng bộ chạy xong, engine xử lý hết microtask rồi mới lấy macrotask tiếp theo.

Câu hỏi kinh điển về thứ tự in

console.log('A');setTimeout(() => console.log('B'), 0);Promise.resolve().then(() => console.log('C'));console.log('D');

Kết quả là A, D, C, B. Code đồng bộ (A, D) chạy trước, sau đó microtask của Promise (C), cuối cùng mới đến macrotask của setTimeout (B), dù thời gian chờ bằng 0.

Promise là gì?

Promise là đối tượng đại diện cho kết quả của một thao tác bất đồng bộ, có ba trạng thái: pending (đang chờ), fulfilled (thành công) và rejected (thất bại). Promise giúp tránh callback lồng nhau nhiều tầng và cho phép nối chuỗi bằng then.

function wait(ms) {  return new Promise((resolve) => setTimeout(resolve, ms));}wait(500)  .then(() => console.log('Xong sau 500ms'))  .catch((err) => console.error(err));

async/await

async/await là cú pháp giúp viết code bất đồng bộ trông như đồng bộ. Hàm async luôn trả về Promise, còn await tạm dừng hàm đó cho đến khi Promise hoàn tất mà không chặn luồng chính.

async function loadUser(id) {  try {    const res = await fetch('/api/users/' + id);    if (!res.ok) throw new Error('HTTP ' + res.status);    return await res.json();  } catch (err) {    console.error('Lỗi tải người dùng', err);    return null;  }}

Chạy tuần tự hay song song?

Một lỗi phổ biến là dùng await liên tiếp cho các tác vụ độc lập khiến chúng chạy tuần tự, chậm hơn cần thiết. Khi các tác vụ không phụ thuộc nhau, hãy chạy song song:

const [users, posts] = await Promise.all([  fetch('/api/users').then((r) => r.json()),  fetch('/api/posts').then((r) => r.json())]);

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ó lỗi, dùng Promise.allSettled. Promise.race trả về kết quả của Promise xong đầu tiên.

Những câu hỏi nhỏ hay gặp

  • Callback hell là gì? Là cảnh các callback lồng nhau nhiều tầng khó đọc, khó xử lý lỗi; Promise và async/await giải quyết vấn đề này.
  • Quên await thì sao? Bạn nhận về một Promise chưa hoàn tất thay vì giá trị, dễ gây lỗi khó tìm.
  • Lỗi trong Promise không có catch? Sẽ trở thành unhandled rejection, có thể làm hỏng tiến trình Node.js tùy cấu hình.

Ví dụ xử lý lỗi và thử lại

Trong dự án thật, bạn thường phải thử lại khi mạng không ổn định. Một hàm đơn giản dùng async/await và vòng lặp có thể làm việc này gọn gàng:

async function retry(fn, times = 3, delay = 300) {  let lastError;  for (let i = 0; i < times; i++) {    try {      return await fn();    } catch (err) {      lastError = err;      await new Promise((r) => setTimeout(r, delay));    }  }  throw lastError;}

Khi giải thích, hãy nhấn mạnh rằng await trong vòng lặp ở đây là có chủ đích vì các lần thử phải tuần tự. Người phỏng vấn thích thấy bạn phân biệt được khi nào nên tuần tự và khi nào nên song song. Bạn cũng nên nhắc rằng việc thử lại cần giới hạn số lần và có thể thêm thời gian chờ tăng dần để không làm quá tải máy chủ.

Cách ôn luyện

Hãy tự viết ba đoạn code có nhiều console.log trộn setTimeout, Promise và async/await rồi dự đoán thứ tự. Sau đó chạy để kiểm tra. Chỉ sau vài lần như vậy, bạn sẽ thấy các câu đố về thứ tự thực thi trở nên quen thuộc.

Kết luận

Khi hiểu event loop và hai hàng đợi, bạn có thể giải thích hầu hết câu đố về thứ tự thực thi. Hãy tự chạy các ví dụ, thay đổi dần và đoán trước kết quả để nhớ lâu.

Nếu bạn muốn học JavaScript hiện đại một cách có hệ thống, hãy xem các khóa phù hợp tại codebem.com.

author_photo
Nguyễn Đình Cường

0 comment