Promise và async/await: xử lý bất đồng bộ không còn rối

  • author-image

    Nguyễn Đình Cường

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

Lấy dữ liệu từ máy chủ, đọc file, chờ một khoảng thời gian: những việc này mất thời gian, và JavaScript không muốn đứng im chờ vì giao diện sẽ bị đơ. Lập trình bất đồng bộ giải quyết điều đó. Promise và cú pháp async/await giúp bạn viết mã bất đồng bộ đọc như mã tuần tự thông thường.

Vấn đề với callback

Cách làm xưa là truyền một hàm callback sẽ được gọi khi việc xong. Khi nhiều bước phụ thuộc nhau, mã lồng sâu thành "callback hell":

layUser(function (user) {
  layDonHang(user.id, function (donHang) {
    layChiTiet(donHang[0].id, function (chiTiet) {
      console.log(chiTiet);
    });
  });
});

Khó đọc, và việc xử lý lỗi ở mỗi tầng rất vất vả.

Promise là gì?

Promise là một đối tượng đại diện cho kết quả sẽ có trong tương lai. Nó ở một trong ba trạng thái: đang chờ (pending), thành công (fulfilled) hoặc thất bại (rejected). Bạn tạo promise và xử lý như sau:

function doi(ms) {
  return new Promise(function (resolve) {
    setTimeout(resolve, ms);
  });
}

doi(1000)
  .then(function () { console.log('Đã đợi 1 giây'); })
  .catch(function (loi) { console.error(loi); })
  .finally(function () { console.log('Xong'); });

then nhận kết quả khi thành công, catch bắt lỗi, finally luôn chạy. Mỗi then trả về promise mới nên bạn nối chuỗi được, thay cho việc lồng nhau.

async/await: cú pháp dễ đọc hơn

Từ khóa async trước hàm làm hàm luôn trả về promise. Bên trong, await tạm dừng hàm cho đến khi promise hoàn thành rồi trả về giá trị:

async function taiDuLieu() {
  try {
    const user = await layUser();
    const donHang = await layDonHang(user.id);
    const chiTiet = await layChiTiet(donHang[0].id);
    console.log(chiTiet);
  } catch (loi) {
    console.error('Có lỗi:', loi);
  } finally {
    console.log('Kết thúc');
  }
}

So với callback, mã đọc từ trên xuống dưới, và một khối try/catch xử lý lỗi của mọi bước. Lưu ý await chỉ dùng được bên trong hàm async (hoặc ở cấp cao nhất của module).

Chạy song song với Promise.all

Một sai lầm hiệu năng phổ biến là await tuần tự những việc không phụ thuộc nhau:

// Chậm: chạy lần lượt
const a = await layA();
const b = await layB();

// Nhanh hơn: chạy song song
const [ketQuaA, ketQuaB] = await Promise.all([layA(), layB()]);

Promise.all chờ tất cả hoàn thành và thất bại ngay nếu một cái lỗi. Nếu muốn nhận kết quả của tất cả dù có cái lỗi, dùng Promise.allSettled. Còn Promise.race trả về kết quả của promise xong đầu tiên, hữu ích khi đặt thời gian chờ tối đa.

Các lỗi thường gặp

  1. Quên await: bạn nhận về một promise chứ không phải giá trị, thường thấy Promise { pending } khi in ra.
  2. Dùng await trong forEach: forEach không chờ các hàm async. Hãy dùng vòng for...of nếu cần tuần tự, hoặc Promise.all với map nếu muốn song song.
  3. Nuốt lỗi: một promise bị reject mà không có catch sẽ gây lỗi unhandled rejection. Luôn xử lý lỗi ở nơi thích hợp.
  4. Không trả về promise trong then: làm chuỗi không chờ bước đó.

Vòng lặp sự kiện trong một câu

JavaScript chạy trên một luồng. Khi gặp việc bất đồng bộ như timer hoặc yêu cầu mạng, nó giao cho môi trường (trình duyệt) xử lý, rồi tiếp tục chạy mã khác. Khi việc xong, callback được xếp hàng đợi và chạy khi luồng rảnh. Callback của promise thuộc hàng đợi ưu tiên cao hơn timer. Đó là lý do setTimeout(fn, 0) vẫn chạy sau các then đã sẵn sàng.

Ví dụ: thử lại khi thất bại

Mạng không ổn định là chuyện thường. Một hàm thử lại vài lần trước khi bỏ cuộc là ví dụ tốt để luyện async/await:

const doi = (ms) => new Promise((r) => setTimeout(r, ms));

async function thuLai(tacVu, soLan = 3) {
  for (let i = 1; i <= soLan; i++) {
    try {
      return await tacVu();
    } catch (loi) {
      if (i === soLan) throw loi;
      await doi(500 * i);   // chờ lâu dần sau mỗi lần lỗi
    }
  }
}

thuLai(() => fetch('/api/du-lieu').then(r => r.json()));

Hàm này gọi lại tác vụ, tăng thời gian chờ sau mỗi lần thất bại và chỉ ném lỗi ở lần cuối. Hãy chỉ thử lại với những thao tác an toàn khi lặp lại, ví dụ lấy dữ liệu, không nên tự động thử lại việc thanh toán.

Kết luận

Promise biểu diễn kết quả tương lai, async/await giúp viết mã dễ đọc, try/catch xử lý lỗi gọn và Promise.all tăng tốc cho các việc độc lập. Hãy tập chuyển các đoạn dùng callback cũ sang async/await để thấy sự khác biệt.

Để áp dụng vào việc gọi API thực tế, bạn có thể tham khảo tiếp các khóa học trên codebem.com.

author_photo
Nguyễn Đình Cường

0 comment