JavaScript & nền tảng7 phút đọc · 31 thg 5, 2026

async/await và Promise trong JavaScript: hiểu một lần, dùng cả đời

✦ Tóm tắt bởi AI

Bài viết giải thích ba cách xử lý bất đồng bộ trong JavaScript: callback (dễ rơi vào callback hell), Promise với .then()/.catch() (code phẳng và gom lỗi tốt), và async/await (cú pháp đường giúp viết bất đồng bộ như đồng bộ). Các developer nên nắm chắc nhóm này vì chúng xuất hiện ở mọi dòng code React và Node.js thực tế, kèm bẫy phổ biến như quên await hay không chạy song song với Promise.all().

HOLETEX · POST
ASYNC / AWAIT
& Promise

Khi mới học JavaScript, sớm muộn bạn cũng gặp ba từ khóa làm đau đầu nhiều người: Promise, async, await. Gọi API, đọc file, chờ database trả kết quả: tất cả đều là bất đồng bộ (asynchronous). Hiểu nhóm khái niệm này một lần cho chắc sẽ tiết kiệm cho bạn hàng trăm giờ debug sau này. Bài này giải thích từ gốc, code ngắn và đúng, đối chiếu trực tiếp với tài liệu MDN.

Bất đồng bộ trong JavaScript là gì, và vì sao cần nó

JavaScript chạy trên một luồng (single-threaded): tại một thời điểm nó chỉ làm được một việc. Vấn đề là nhiều thao tác lại tốn thời gian mà không phụ thuộc vào CPU của bạn: gọi một REST API, đọc file trên ổ cứng, chờ phản hồi từ server. Nếu code phải "đứng chờ" (blocking) cho đến khi mạng trả về, cả trang web sẽ đơ: không click được, không cuộn được, không gõ được gì.

Bất đồng bộ là cách giải bài toán đó. Thay vì đứng chờ kết quả, bạn nói với JavaScript: "khởi động việc này đi, khi nào xong thì báo tôi", rồi code chạy tiếp những việc khác. Khi kết quả sẵn sàng, phần xử lý của bạn mới được gọi. Nhờ vậy giao diện vẫn mượt trong lúc dữ liệu đang được tải về.

Ý tưởng thì đơn giản, nhưng cách viết nó trong JavaScript đã tiến hóa qua ba thế hệ: callback, rồi Promise, rồi async/await.

Thế hệ đầu: callback và "callback hell"

Cách cũ nhất là truyền một hàm (callback) vào để JavaScript gọi lại khi xong việc. Một tầng thì ổn. Nhưng khi các việc phụ thuộc nhau, bạn phải lồng callback vào callback, tạo ra thứ huyền thoại tên là callback hell (hay "pyramid of doom"):

js
doSomething(function (result) {
  doSomethingElse(result, function (newResult) {
    doThirdThing(newResult, function (finalResult) {
      console.log(`Got the final result: ${finalResult}`);
    }, failureCallback);
  }, failureCallback);
}, failureCallback);

Code thụt vào sâu dần như một cái phễu, đọc đã khó, xử lý lỗi (cái failureCallback lặp ở mỗi tầng) còn khổ hơn. Đây chính là lý do Promise ra đời.

Thế hệ hai: Promise (.then / .catch)

Một Promise là một object đại diện cho kết quả của một thao tác bất đồng bộ sẽ hoàn tất trong tương lai: có thể thành công (fulfilled) hoặc thất bại (rejected). Hiểu nôm na: đó là một "tờ phiếu hẹn", giờ chưa có hàng nhưng cam kết sẽ trả về thứ gì đó.

Thay vì nhét callback vào trong hàm, bạn gắn xử lý vào Promise mà hàm trả về, bằng .then() cho trường hợp thành công và .catch() cho lỗi:

js
doSomething()
  .then((result) => doSomethingElse(result))
  .then((newResult) => doThirdThing(newResult))
  .then((finalResult) => {
    console.log(`Got the final result: ${finalResult}`);
  })
  .catch(failureCallback);

So với callback hell, đây là một trời một vực. Code đi xuống theo chiều dọc phẳng đẹp thay vì lồng vào nhau. Quan trọng nhất: chỉ cần một .catch() ở cuối là bắt được lỗi của bất kỳ bước nào trong chuỗi, vì Promise tự động đẩy lỗi xuôi xuống cho đến khi gặp chỗ xử lý.

Mỗi .then() lại trả về một Promise mới, nên bạn xâu chuỗi (chaining) được nhiều bước. Một quy tắc đáng nhớ: luôn return Promise bên trong .then(), nếu quên thì bước sau sẽ nhận undefined.

Cú pháp arrow function (result) => ... ở trên là của JavaScript ES6, cùng thời với Promise. Nếu phần này còn lạ, bạn nên nắm vững ES6 trước.

Thế hệ ba: async/await, viết bất đồng bộ như đồng bộ

async await là gì

async/awaitcú pháp đường (syntactic sugar) xây trên Promise. Nó không thay thế Promise, mà cho phép bạn viết code bất đồng bộ trông gần như code đồng bộ bình thường, dễ đọc hơn hẳn chuỗi .then().

Hai từ khóa cần nhớ:

  • async đặt trước một hàm, biến hàm đó thành hàm bất đồng bộ. Hàm async luôn trả về một Promise, kể cả khi bạn return một giá trị thường (nó được bọc tự động trong Promise.resolve()).
  • await đặt trước một Promise, tạm dừng hàm cho đến khi Promise đó settle (xong), rồi lấy ra giá trị kết quả. await chỉ dùng được bên trong hàm async (hoặc ở top-level của module).

Cùng một logic ba bước ở trên, viết bằng async/await sẽ thành:

js
async function getResult() {
  const result = await doSomething();
  const newResult = await doSomethingElse(result);
  const finalResult = await doThirdThing(newResult);
  console.log(`Got the final result: ${finalResult}`);
}

Không còn .then() xếp tầng, không còn lồng nhau. Đọc từ trên xuống như đọc văn xuôi: làm cái này, lấy kết quả, làm cái tiếp theo. Đây là lý do async/await thống trị code JavaScript hiện đại, cả ở frontend lẫn trên server với Node.js.

Một ví dụ cụ thể hơn để thấy await "chờ" thật:

js
function resolveAfter2Seconds() {
  return new Promise((resolve) => {
    setTimeout(() => resolve("xong rồi"), 2000);
  });
}

async function demo() {
  console.log("bắt đầu gọi");
  const ketqua = await resolveAfter2Seconds();
  console.log(ketqua); // "xong rồi", in ra sau 2 giây
}

Xử lý lỗi: try/catch với async/await

Điểm đẹp nữa của async/await: bạn dùng lại try/catch quen thuộc của code đồng bộ, không cần học cú pháp lỗi riêng. Cái gì await mà bị reject sẽ nhảy thẳng vào catch:

js
async function getProcessedData(url) {
  try {
    const data = await downloadData(url);
    return processData(data);
  } catch (e) {
    console.error("Tải dữ liệu thất bại:", e);
    const fallback = await downloadFallbackData(url);
    return processData(fallback);
  }
}

Cùng một khối try/catch bao được nhiều bước await liền nhau, giống hệt cách .catch() bắt lỗi cho cả chuỗi Promise. Gọn gàng và quen tay.

Chạy song song với Promise.all

Một cái bẫy hiệu năng phổ biến: await lần lượt những việc không phụ thuộc nhau. Ví dụ tải ba thứ độc lập:

js
// CHẬM: chờ xong cái 1 mới bắt đầu cái 2, xong cái 2 mới tới cái 3
const user = await getUser();
const posts = await getPosts();
const comments = await getComments();

Ba việc này không cần kết quả của nhau, nhưng code trên bắt chúng xếp hàng chờ lần lượt. Nếu mỗi cái mất 1 giây thì tổng là 3 giây.

Promise.all() khởi động cả ba cùng lúc rồi chờ tất cả xong, tổng thời gian chỉ bằng cái lâu nhất:

js
// NHANH: ba việc chạy song song
const [user, posts, comments] = await Promise.all([
  getUser(),
  getPosts(),
  getComments(),
]);

Lưu ý: Promise.all() sẽ reject ngay khi một Promise bất kỳ thất bại. Nếu bạn muốn chờ tất cả xong rồi mới xem cái nào thành công cái nào lỗi, dùng Promise.allSettled(). Còn Promise.race() trả về kết quả của cái nào settle đầu tiên (hữu ích cho timeout).

Lỗi thường gặp: quên await

Đây là lỗi số một với người mới, và đáng sợ ở chỗ nó thường không báo lỗi gì cả, chỉ ra kết quả sai:

js
async function getUserName() {
  const user = getUser(); // QUÊN await
  console.log(user.name); // undefined, vì user là một Promise chứ không phải dữ liệu
}

Khi quên await, biến user giữ chính cái Promise (tờ phiếu hẹn), chứ không phải món hàng bên trong. Truy cập user.name ra undefined. Sửa lại chỉ là thêm await:

js
const user = await getUser();
console.log(user.name); // đúng

Vài lỗi hay đi kèm khác để bạn để ý:

  • Dùng await ngoài hàm async: sẽ báo SyntaxError. await phải nằm trong hàm async (hoặc top-level module).
  • Quên return trong .then(): bước sau nhận undefined. Với async/await thì cứ await rồi gán vào biến là tránh được.
  • Không bắt lỗi: một Promise reject mà không có .catch() hay try/catch sẽ thành "unhandled rejection". Luôn bao thao tác có thể fail (như gọi API) trong try/catch.
  • Quên rằng callback của Promise luôn chạy bất đồng bộ: Promise.resolve().then(() => console.log(2)); console.log(1); in ra 1 rồi mới 2, vì các then callback là microtask, chạy sau khi call stack hiện tại rỗng.

Tóm lại

  • JavaScript chạy single-threaded, nên cần bất đồng bộ để không bị đơ khi chờ mạng, file, database.
  • Callback giải được bài toán nhưng lồng nhau thành callback hell.
  • Promise làm phẳng code với .then() / .catch() và gom lỗi về một chỗ.
  • async/await là lớp đường trên Promise, cho code bất đồng bộ đọc như code đồng bộ, dùng try/catch quen thuộc để bắt lỗi.
  • Dùng Promise.all() cho các việc độc lập để chạy song song, và luôn nhớ await.

Nắm chắc nhóm này, bạn xử lý được phần lớn tình huống thực tế: gọi API, tải dữ liệu, xử lý sự kiện. Đây là kiến thức nền dùng hằng ngày dù bạn theo React, Node.js hay bất kỳ hướng nào.


Nền JavaScript vững là chìa khóa. Promise và async/await xuất hiện ở mọi dòng code React lẫn backend thực tế. Nếu bạn muốn đi từ nền tảng JavaScript đến dự án React đi làm được, học theo lộ trình có cấu trúc thay vì ghép nhặt trăm video rời rạc, hãy xem khóa React PRO của HoleTex. Còn nếu muốn rèn tư duy logic và giải thuật để qua phỏng vấn cho ngọt, luyện thêm ở HoleTex Algo.

Bài liên quan

Nguồn tham khảo: MDN Web Docs - "Using promises" (developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises), "async function" (developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function), "Promise" (developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). Đối chiếu 2026-06-04.

Thấy hay? Chia sẻ