JavaScript Visualized: Promise và Async/Await
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:
- trạng thái chạy có quy tắc rõ ràng,
- luồng xử lý có thể chain,
- error propagation thống nhất,
- 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:
- lồng hàm sâu,
- xử lý lỗi phân tán,
- 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:
pending->fulfilledpending->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:
- return value thường -> Promise sau resolve value đó,
- throw error -> Promise sau reject,
- 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:
- hoàn tất synchronous call stack hiện tại,
- drain microtask queue (
Promise.then,queueMicrotask), - mới tới task queue tiếp theo (timer, I/O callback, UI event).
EVENT LOOP: TASK VS MICROTASK
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.
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
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.
(empty)
(empty)
Console Output
A
Final order: A D C B
Giải thích ngắn:
AvàDlà synchronous nên chạy ngay trên Call Stack.Promise.thenvào Microtask Queue nên được ưu tiên xử lý trước.setTimeoutvào Task Queue nên chạy sau khi microtask đã drain.- 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:
- function tạm dừng,
- trả quyền điều khiển về caller,
- phần tiếp theo được đưa vào microtask sau khi Promise được await settle.
ASYNC/AWAIT EXECUTION FLOW
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
- Quên
awaitkhiến luồng đi tiếp khi Promise chưa xong. - Dùng
awaittrong loop làm serialize không cần thiết. - Trộn
.then()vàawaitgây khó đọc và khó truy lỗi. catchrỗng làm nuốt lỗi.- 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:
- Cần tất cả thành công: dùng
all. - Cần báo cáo đủ cả thành công/thất bại: dùng
allSettled. - Cần kết quả đến trước (kể cả fail): dùng
race. - Cần kết quả thành công đầu tiên: dùng
any.
Promise.all([...]) - all-or-nothing
Cơ chế:
- Resolve khi tất cả phần tử đều fulfilled.
- Reject ngay khi gặp phần tử reject đầu tiên (fail-fast).
- Giá trị trả về giữ đúng thứ tự input, không phải thứ tự hoàn thành.
Khi nên dùng:
- Trang dashboard chỉ render khi đủ user, permissions, settings.
- Pipeline mà thiếu một bước thì kết quả tổng không hợp lệ.
Điểm dễ sai:
- 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à
allreject sớm.
const [user, perms, settings] = await Promise.all([
fetchUser(),
fetchPermissions(),
fetchSettings(),
]);Promise.allSettled([...]) - lấy báo cáo đầy đủ
Cơ chế:
- Luôn chờ tất cả Promise settle.
- Không ném lỗi fail-fast.
- Trả về mảng object dạng:
{"status":"fulfilled","value":...}hoặc{"status":"rejected","reason":...}.
Khi nên dùng:
- Gọi 10 service độc lập, muốn hiện service nào có dữ liệu thì hiện trước.
- 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ế:
- Promise đầu tiên settle sẽ quyết định kết quả
race. - Nếu phần tử về đầu là reject thì
racereject luôn.
Khi nên dùng:
- Timeout guard cho request chậm.
- 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ế:
- Trả về fulfilled đầu tiên.
- Bỏ qua các rejection trung gian.
- Chỉ reject khi tất cả đều reject, với
AggregateError.
Khi nên dùng:
- Multi-region fallback.
- 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:
- state machine của Promise (
pending -> fulfilled/rejected), - thứ tự scheduling (microtask chạy trước task kế tiếp),
- 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.