JavaScript Visualized: Promise và Async/Await

Hoang Vu,5 min read

Ngày 25 tháng 3, 2026

Bài viết tập trung vào mô hình trực quan và checklist thực chiến để bạn đọc luồng bất đồng bộ và khoanh vùng lỗi nhanh trong production.


1. Vì sao Promise quan trọng

Promise là abstraction chuẩn của JavaScript cho dữ liệu sẽ có trong tương lai.

Nếu không có Promise, code bất đồng bộ rất dễ rơi vào callback lồng nhau khó bảo trì. Promise giúp:

  1. trạng thái chạy có quy tắc rõ ràng,
  2. luồng xử lý có thể chain,
  3. error propagation thống nhất,
  4. tương thích trực tiếp với async/await.

Callback là gì và vì sao dễ rối?

Callback là function được truyền vào function khác để gọi lại khi tác vụ hoàn tất.

fetchUser((err, user) => {
  if (err) return handleError(err);
  fetchPosts(user.id, (err2, posts) => {
    if (err2) return handleError(err2);
    fetchComments(posts[0].id, (err3, comments) => {
      if (err3) return handleError(err3);
      render(comments);
    });
  });
});

Mẫu này hoạt động được, nhưng khi nhiều bước phụ thuộc lẫn nhau thì rất nhanh thành callback hell:

  1. lồng hàm sâu,
  2. xử lý lỗi phân tán,
  3. khó test và khó refactor.

Promise ra đời để giải quyết chính vấn đề này bằng chain và một đường truyền lỗi thống nhất.


2. State machine của Promise

Promise chỉ có chuyển trạng thái một chiều:

  1. pending -> fulfilled
  2. pending -> rejected

Khi đã settle, Promise không thể quay lại pending hoặc đổi nhánh.

PROMISE STATE MACHINE

pending

fulfilled

rejected

Code Mapping

const p = fetch('/api/profile');
p.then((data) => renderProfile(data))
.catch((error) => reportError(error));

pending: Promise is still waiting for an async result.

Current: pending

Tính bất biến này là nền tảng giúp Promise chain dễ dự đoán.


3. Cơ chế then/catch/finally

then, catch, finally luôn trả về một Promise mới.

Điều đó có nghĩa mỗi handler tạo thêm một "stage" trong chain:

  1. return value thường -> Promise sau resolve value đó,
  2. throw error -> Promise sau reject,
  3. return Promise -> chain chờ Promise đó settle.

Đây là lý do Promise composition mạnh và linh hoạt.


4. Event loop và microtask queue

Rất nhiều bug Promise thực ra là bug hiểu sai event loop.

Quy tắc tổng quát:

  1. hoàn tất synchronous call stack hiện tại,
  2. drain microtask queue (Promise.then, queueMicrotask),
  3. mới tới task queue tiếp theo (timer, I/O callback, UI event).

EVENT LOOP: TASK VS MICROTASK

Task Queue
setTimeout callback
UI event callback
network callback
Microtask Queue
promise.then
queueMicrotask
mutation observer

Code Mapping

console.log('start');
setTimeout(() => console.log('task'), 0);
Promise.resolve().then(() => console.log('microtask'));
console.log('end');

Microtask is executed before the next task.

Microtasks are drained before the next task is picked.

Vì vậy callback từ Promise thường chạy trước setTimeout(fn, 0).

Ví dụ quiz: vì sao ra A D C B?

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

EVENT LOOP QUIZ: WHY A D C B

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

Step 1: Synchronous line runs first: log A.

Microtask Queue

(empty)

Task Queue

(empty)

Console Output

A

Final order: A D C B

Giải thích ngắn:

  1. AD là synchronous nên chạy ngay trên Call Stack.
  2. Promise.then vào Microtask Queue nên được ưu tiên xử lý trước.
  3. setTimeout vào Task Queue nên chạy sau khi microtask đã drain.
  4. Kết quả cuối cùng là A D C B.

5. Async/await là syntax sugar của Promise

async/await không thay thế Promise, mà chỉ diễn đạt Promise theo kiểu tuần tự, dễ đọc hơn.

Khi execution chạm await:

  1. function tạm dừng,
  2. trả quyền điều khiển về caller,
  3. phần tiếp theo được đưa vào microtask sau khi Promise được await settle.

ASYNC/AWAIT EXECUTION FLOW

1. Function starts
2. await promise
3. resume in microtask
4. continue after await

Code Mapping

async function loadUser() {
const res = await fetch('/api/user');
const data = await res.json();
return data;
}

Function enters and returns a Promise immediately.

Do đó await nhìn giống đồng bộ, nhưng runtime vẫn là bất đồng bộ.


6. Các lỗi thường gặp trong production

  1. Quên await khiến luồng đi tiếp khi Promise chưa xong.
  2. Dùng await trong loop làm serialize không cần thiết.
  3. Trộn .then()await gây khó đọc và khó truy lỗi.
  4. catch rỗng làm nuốt lỗi.
  5. Unhandled rejection không được đưa vào monitoring.

7. Pattern triển khai thực dụng

Các loại Promise combinator quan trọng

Mẹo nhớ nhanh:

  1. Cần tất cả thành công: dùng all.
  2. Cần báo cáo đủ cả thành công/thất bại: dùng allSettled.
  3. Cần kết quả đến trước (kể cả fail): dùng race.
  4. Cần kết quả thành công đầu tiên: dùng any.

Promise.all([...]) - all-or-nothing

Cơ chế:

  1. Resolve khi tất cả phần tử đều fulfilled.
  2. Reject ngay khi gặp phần tử reject đầu tiên (fail-fast).
  3. Giá trị trả về giữ đúng thứ tự input, không phải thứ tự hoàn thành.

Khi nên dùng:

  1. Trang dashboard chỉ render khi đủ user, permissions, settings.
  2. Pipeline mà thiếu một bước thì kết quả tổng không hợp lệ.

Điểm dễ sai:

  1. Nhiều người nghĩ Promise còn lại bị "hủy" khi có reject. Thực tế không tự hủy, chỉ là all reject sớm.
const [user, perms, settings] = await Promise.all([
  fetchUser(),
  fetchPermissions(),
  fetchSettings(),
]);

Promise.allSettled([...]) - lấy báo cáo đầy đủ

Cơ chế:

  1. Luôn chờ tất cả Promise settle.
  2. Không ném lỗi fail-fast.
  3. Trả về mảng object dạng: {"status":"fulfilled","value":...} hoặc {"status":"rejected","reason":...}.

Khi nên dùng:

  1. Gọi 10 service độc lập, muốn hiện service nào có dữ liệu thì hiện trước.
  2. Thu thập kết quả batch job để log và retry có chọn lọc.
const settled = await Promise.allSettled([
  fetchAnalytics(),
  fetchRecommendations(),
  fetchNotifications(),
]);
 
const ok = settled
  .filter((r): r is PromiseFulfilledResult<unknown> => r.status === "fulfilled")
  .map((r) => r.value);

Promise.race([...]) - ai settle trước thì thắng

Cơ chế:

  1. Promise đầu tiên settle sẽ quyết định kết quả race.
  2. Nếu phần tử về đầu là reject thì race reject luôn.

Khi nên dùng:

  1. Timeout guard cho request chậm.
  2. Chọn nguồn phản hồi nhanh nhất khi chấp nhận cả khả năng fail nhanh.
const data = await Promise.race([
  fetch("/api/heavy").then((r) => r.json()),
  new Promise((_, reject) =>
    setTimeout(() => reject(new Error("Timeout after 1500ms")), 1500)
  ),
]);

Promise.any([...]) - thành công đầu tiên mới thắng

Cơ chế:

  1. Trả về fulfilled đầu tiên.
  2. Bỏ qua các rejection trung gian.
  3. Chỉ reject khi tất cả đều reject, với AggregateError.

Khi nên dùng:

  1. Multi-region fallback.
  2. Nhiều CDN mirror, lấy endpoint sống đầu tiên.
const profile = await Promise.any([
  fetchFromRegion("ap-southeast"),
  fetchFromRegion("us-east"),
  fetchFromRegion("eu-west"),
]);
try {
  await Promise.any([svcA(), svcB(), svcC()]);
} catch (err) {
  if (err instanceof AggregateError) {
    console.error("All providers failed", err.errors);
  }
}

Chạy song song

const [user, posts, settings] = await Promise.all([
  fetchUser(),
  fetchPosts(),
  fetchSettings(),
]);

Chạy song song có chấp nhận fail cục bộ

const results = await Promise.allSettled([
  fetchServiceA(),
  fetchServiceB(),
  fetchServiceC(),
]);

Bọc timeout rõ ràng

function withTimeout<T>(p: Promise<T>, ms: number): Promise<T> {
  return Promise.race([
    p,
    new Promise<T>((_, reject) =>
      setTimeout(() => reject(new Error("Timeout")), ms)
    ),
  ]);
}

8. Mô hình tư duy nên giữ

Khi debug async code, hãy tách theo 3 lớp:

  1. state machine của Promise (pending -> fulfilled/rejected),
  2. thứ tự scheduling (microtask chạy trước task kế tiếp),
  3. chiến lược composition (parallel, retry, timeout, fallback).

Đi theo 3 lớp này, phần lớn lỗi async sẽ dễ lần ra nguyên nhân hơn nhiều.

2026 © @hoag/blog.