Mối Liên Hệ Giữa Promise Và Async/Await Trong JavaScript

Avatar
namanh
Junior

Lập trình bất đồng bộ trong JavaScript đã trải qua nhiều giai đoạn tiến hóa, từ những cấu trúc Callback Hell chật vật đến kỷ nguyên của Promise. Tuy nhiên, sự xuất hiện của async/await trong ES8 (2017) mới thực sự mang lại trải nghiệm viết code mượt mà nhất. Nhiều lập trình viên mới thường đặt câu hỏi: "Nên dùng Promise hay Async/Await?". Thực tế, đây không phải là hai phương pháp đối lập. Bản chất của async/await chính là một lớp vỏ bọc hoàn hảo (syntactic sugar) được xây dựng trực tiếp trên nền tảng vững chắc của Promise.

1. Mọi Hàm Async Đều Trả Về Một Promise

Dấu ấn rõ nét nhất chứng minh async/await được sinh ra từ Promise nằm ở chính từ khóa async. Khi bạn khai báo một hàm bắt đầu bằng async, mặc định hàm đó sẽ luôn luôn trả về một đối tượng Promise, cho dù bên trong bạn không hề sử dụng cú pháp new Promise().

Nếu hàm async của bạn trả về một giá trị thông thường (ví dụ: return "Dữ liệu";), engine của JavaScript sẽ tự động bọc giá trị đó lại bằng Promise.resolve("Dữ liệu"). Tương tự, nếu có một lỗi văng ra bên trong hàm, nó sẽ ngầm định tương đương với Promise.reject(). Điều này minh chứng rằng async không tạo ra một chuẩn mực mới, nó chỉ chuẩn hóa đầu ra để luôn tương thích với hệ sinh thái Promise.

2. Await: Chiếc Chìa Khóa "Mở Khóa" Promise

Sự lột xác lớn nhất về mặt cú pháp nằm ở từ khóa await. Trước đây, để trích xuất dữ liệu từ một Promise khi nó thành công (fulfilled), bạn bắt buộc phải gọi phương thức .then() và truyền vào một hàm callback. Việc lồng ghép nhiều .then() nối tiếp nhau tuy đã gọn hơn callback cũ nhưng vẫn có thể gây rối mắt nếu logic xử lý bên trong quá phức tạp.

Từ khóa await sinh ra để thay thế hoàn toàn chuỗi .then() đó. Khi đặt await trước một Promise (như một lời gọi API bằng fetch), luồng thực thi của hàm sẽ tạm dừng ngay tại dòng code đó cho đến khi Promise được giải quyết (settled). Dữ liệu trả về sẽ được gán trực tiếp vào một biến. Nhờ vậy, code bất đồng bộ giờ đây có thể được đọc từ trên xuống dưới một cách tuần tự, mang hình hài của code đồng bộ truyền thống.

3. Xử Lý Lỗi Đồng Nhất Với Try/Catch

Một sự khác biệt lớn khác thể hiện sức mạnh của async/await là cách quản lý lỗi. Trong kiến trúc thuần Promise, bạn phải nối thêm .catch() ở cuối chuỗi để gom các lỗi phát sinh.

Với async/await, do luồng code được viết theo phong cách đồng bộ, bạn có thể tận dụng ngay cấu trúc try/catch truyền thống của JavaScript. Khối catch sẽ tóm gọn mọi lỗi xảy ra, không phân biệt đó là lỗi mạng từ một Promise bị từ chối (rejected) hay lỗi logic thông thường (như gọi sai tên biến, chia cho 0). Sự hợp nhất này giúp việc gỡ lỗi (debugging) trở nên trực quan và dễ kiểm soát hơn rất nhiều so với việc phải theo dõi qua các chuỗi .catch() tách biệt.

4. Sự Kết Hợp Hoàn Hảo Thay Vì Loại Trừ

Hiểu được mối liên hệ gốc rễ này, bạn sẽ nhận ra chúng ta không bao giờ "bỏ rơi" Promise để chuyển hẳn sang async/await. Trong thực tế dự án, cả hai luôn đồng hành bổ trợ cho nhau.

Async/await giúp logic xử lý của từng chức năng trở nên dễ đọc và dễ bảo trì. Tuy nhiên, từ khóa await bản chất là xử lý tuần tự (chờ xong cái này mới chạy cái kia). Khi bạn cần tối ưu hiệu suất bằng cách gọi 3 API không liên quan cùng một lúc, bạn bắt buộc phải gọi đến phương thức tĩnh của Promise là Promise.all(). Lúc này, bạn sẽ dùng Promise.all() để gộp 3 tác vụ thành một "siêu Promise", và dùng await để chờ "siêu Promise" đó hoàn thành.

Tóm lại, Promise cung cấp bộ máy động cơ cốt lõi để xử lý các luồng dữ liệu bất đồng bộ, còn async/await chính là chiếc vô lăng tinh tế giúp lập trình viên điều khiển bộ máy đó một cách thoải mái và an toàn nhất. Việc nắm vững cả hai là điều kiện tiên quyết để xây dựng các ứng dụng web hiệu suất cao.