Gần như mọi ứng dụng JavaScript hiện đại đều phải chờ đợi một thứ gì đó: phản hồi từ API, dữ liệu từ database, hay một file đang được đọc. Promise và cú pháp async/await là cách ngôn ngữ này xử lý sự chờ đợi đó mà không làm "đơ" chương trình. Tuy vậy, rất nhiều lập trình viên dùng await hằng ngày nhưng vẫn lúng túng khi gặp lỗi bị nuốt mất, vòng lặp chạy chậm bất thường hay thứ tự log không như mong đợi. Bài viết này giúp bạn hiểu đúng bản chất để viết code bất đồng bộ tự tin hơn.
Promise là gì?
Promise là một object đại diện cho kết quả sẽ có trong tương lai của một tác vụ bất đồng bộ. Tại mỗi thời điểm, một Promise ở một trong ba trạng thái:
- pending: đang chờ, chưa có kết quả.
- fulfilled: hoàn thành thành công, có giá trị trả về.
- rejected: thất bại, có lý do lỗi.
Khi đã chuyển sang fulfilled hoặc rejected, Promise được coi là settled và không bao giờ đổi trạng thái nữa. Bạn đăng ký xử lý kết quả bằng .then(), bắt lỗi bằng .catch() và dọn dẹp bằng .finally().
fetch("/api/products")
.then((res) => res.json())
.then((data) => console.log(data))
.catch((err) => console.error("Lỗi:", err))
.finally(() => hideSpinner());Điểm mấu chốt: mỗi lần gọi .then() sẽ trả về một Promise mới. Nhờ vậy ta có thể nối chuỗi, và một lỗi xảy ra ở bất kỳ bước nào cũng sẽ "trôi" xuống .catch() gần nhất.
async/await: cú pháp đẹp hơn cho cùng một cơ chế
async/await không thay thế Promise mà được xây trên chính Promise. Một hàm khai báo async luôn trả về Promise. Từ khóa await tạm dừng hàm đó cho tới khi Promise được settled, rồi trả về giá trị hoặc ném ra lỗi.
async function loadProducts() {
try {
const res = await fetch("/api/products");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
console.error("Không tải được sản phẩm:", err);
throw err;
}
}Code đọc từ trên xuống như code đồng bộ, và try/catch quen thuộc hoạt động bình thường. Lưu ý rằng fetch không reject khi server trả về mã 404 hay 500, nên bạn cần tự kiểm tra res.ok như ví dụ trên.
Event loop: vì sao thứ tự chạy đôi khi gây bất ngờ
JavaScript chạy trên một luồng chính. Khi gặp await, hàm hiện tại nhường quyền điều khiển; phần còn lại của hàm được xếp vào hàng đợi microtask và chạy sau khi đoạn code đồng bộ hiện tại kết thúc. Thử đoán kết quả:
console.log("A");
Promise.resolve().then(() => console.log("B"));
setTimeout(() => console.log("C"), 0);
console.log("D");Kết quả là A, D, B, C. Code đồng bộ chạy trước, tiếp đến microtask (callback của Promise), cuối cùng mới tới macrotask như setTimeout. Hiểu điều này giúp bạn giải thích được nhiều hành vi "kỳ lạ" khi debug.
Chạy song song thay vì tuần tự
Lỗi hiệu năng phổ biến nhất là await lần lượt những tác vụ không phụ thuộc nhau:
// Chậm: tổng thời gian = a + b
const user = await getUser(id);
const orders = await getOrders(id);
// Nhanh hơn: chạy đồng thời
const [user2, orders2] = await Promise.all([getUser(id), getOrders(id)]);JavaScript cung cấp bốn phương thức kết hợp Promise, mỗi cái phù hợp một tình huống:
Promise.all: chờ tất cả thành công, reject ngay khi có một Promise thất bại.Promise.allSettled: chờ tất cả settled, trả về trạng thái từng cái, phù hợp khi muốn xử lý kết quả từng phần.Promise.race: lấy kết quả của Promise settled đầu tiên, thường dùng để cài timeout.Promise.any: lấy Promise fulfilled đầu tiên, chỉ reject khi tất cả đều thất bại.
Những lỗi thường gặp
- Quên
await: hàm vẫn chạy tiếp với một Promise thay vì giá trị thật, và lỗi bên trong có thể không được bắt. - Dùng
awaittrongforEach:forEachkhông chờ callback async. Hãy dùngfor...ofnếu cần tuần tự, hoặcPromise.allvớimapnếu muốn song song. - Bỏ quên
.catch(): Promise bị reject mà không ai xử lý sẽ sinh ra cảnh báo unhandled rejection, và trong Node.js có thể làm tiến trình dừng hẳn. - Bọc thừa
new Promise: nếu bên trong đã là Promise, chỉ cần trả về trực tiếp thay vì bọc thêm một lớp, vốn dễ làm mất lỗi. - Chạy song song không giới hạn: gọi
Promise.allvới hàng nghìn request cùng lúc có thể làm quá tải server. Hãy chia lô hoặc dùng thư viện giới hạn số tác vụ đồng thời.
Kết luận
Promise là nền móng, còn async/await là lớp cú pháp giúp nền móng đó dễ đọc hơn. Khi hiểu rằng mọi hàm async đều trả về Promise, rằng await chỉ tạm dừng hàm hiện tại chứ không chặn cả chương trình, và rằng các tác vụ độc lập nên chạy song song, bạn sẽ tránh được phần lớn lỗi khó chịu trong code bất đồng bộ. Hãy luôn tự hỏi hai câu khi viết một dòng await: lỗi ở đây sẽ đi đâu, và dòng này có thật sự cần chờ dòng trước không?






