Callback Hell là gì?

Trong những ngày đầu của JavaScript bất đồng bộ, chúng ta xử lý mọi thứ bằng callback. Mỗi tác vụ bất đồng bộ nhận một hàm callback để gọi khi hoàn thành. Nghe có vẻ ổn — cho đến khi bạn cần nhiều tác vụ liên tiếp nhau.

javascript
// Callback hell — "Pyramid of Doom"
getUserById(userId, function(user) {
  getPostsByUser(user.id, function(posts) {
    getCommentsByPost(posts[0].id, function(comments) {
      getAuthorOfComment(comments[0].id, function(author) {
        // Bắt đầu mất kiểm soát...
        console.log(author);
      }, handleError);
    }, handleError);
  }, handleError);
}, handleError);

Code này không chỉ khó đọc, mà còn khó maintain và debug. Đây là lý do cộng đồng JavaScript đau đầu và đẩy ra giải pháp: Promise.

Promise — Bước tiến đầu tiên

Promise đại diện cho một giá trị sẽ có trong tương lai. Thay vì truyền callback vào mỗi hàm, bạn nhận lại một đối tượng Promise và chain các tác vụ bằng .then().

javascript
// Promise chain — sạch hơn nhiều
getUserById(userId)
  .then(user => getPostsByUser(user.id))
  .then(posts => getCommentsByPost(posts[0].id))
  .then(comments => getAuthorOfComment(comments[0].id))
  .then(author => console.log(author))
  .catch(err => console.error('Lỗi:', err));

Async/Await — Cú pháp thay đổi cuộc chơi

ES2017 giới thiệu async/await, cho phép viết code bất đồng bộ trông giống code đồng bộ. Bên dưới vẫn là Promise, nhưng syntax đẹp hơn rất nhiều.

javascript
async function getAuthorInfo(userId) {
  try {
    const user     = await getUserById(userId);
    const posts    = await getPostsByUser(user.id);
    const comments = await getCommentsByPost(posts[0].id);
    const author   = await getAuthorOfComment(comments[0].id);

    // Có thể dùng cả user lẫn author ở đây!
    return { user, author };
  } catch (err) {
    console.error('Lỗi:', err);
    throw err;
  }
}

Chạy song song với Promise.all

Một sai lầm phổ biến là dùng await tuần tự cho các tác vụ độc lập — làm chậm code không cần thiết.

javascript
// ❌ Tuần tự — tổng ~600ms
const user    = await fetchUser();    // 200ms
const posts   = await fetchPosts();   // 200ms
const weather = await fetchWeather(); // 200ms

// ✅ Song song — tổng ~200ms
const [user, posts, weather] = await Promise.all([
  fetchUser(),
  fetchPosts(),
  fetchWeather(),
]);
Tip: Dùng Promise.allSettled() thay vìPromise.all() khi muốn tất cả tác vụ hoàn thành dù có lỗi hay không.

Xử lý lỗi đúng cách

Một pattern hay là tạo wrapper function trả về tuple [error, data]:

javascript
const to = (promise) =>
  promise
    .then(data => [null, data])
    .catch(err  => [err,  null]);

async function loadProfile(id) {
  const [err, user] = await to(fetchUser(id));
  if (err) { console.error('Không tìm thấy user'); return null; }
  return user;
}

Kết luận

Async/await không phải là “magic” — nó là syntactic sugar trên Promise. Một vài điểm cần nhớ:

  • Dùng Promise.all() cho các tác vụ độc lập chạy song song
  • Luôn handle lỗi — dù bằng try/catch hay .catch()
  • Hàm async luôn trả về Promise, dù bạn không return
  • await chỉ dùng được trong async function