
Chào các Gen Z, hôm nay Creyt sẽ cùng các bạn "giải mã" một trong những "siêu năng lực" của lập trình bất đồng bộ trong Node.js, đó chính là Promises! Nghe tên đã thấy "uy tín" rồi đúng không?
1. Promises là gì mà "hot" vậy? (What are Promises?)
Hãy tưởng tượng thế này nhé: Cuộc sống của chúng ta đầy rẫy những việc phải chờ đợi. Đặt đồ ăn, chờ xe bus, chờ crush rep tin nhắn... Trong lập trình, đặc biệt là với Node.js, những việc như đọc file, gọi API, truy vấn database cũng là những "pha chờ đợi" không kém. Nếu cứ chờ từng cái một theo kiểu "thằng này xong thì thằng kia mới chạy", thì hệ thống của chúng ta sẽ chậm như rùa bò, còn người dùng thì... "bye bye".
Ngày xưa, dân code tụi anh hay dùng Callbacks để xử lý mấy vụ chờ đợi này. Đại loại là "khi nào xong thì gọi tao nhé". Nghe thì ổn, nhưng cứ nhiều lớp Callback lồng vào nhau, nó biến thành cái "mê cung" hay còn gọi là Callback Hell – một mớ bún riêu cua mà nhìn vào là muốn "tổ lái" luôn. Code vừa khó đọc, khó debug, lại còn dễ toang.
Và rồi, Promises xuất hiện như một "vị cứu tinh", một "lời hứa có giấy trắng mực đen" từ tương lai. Nó không phải là thứ có sẵn ngay lập tức, mà là một object đại diện cho một giá trị sẽ có sẵn (hoặc không) trong tương lai.
Nghĩa là sao? Khi bạn thực hiện một thao tác bất đồng bộ (ví dụ: gọi API), hàm đó sẽ không trả về dữ liệu ngay. Thay vào đó, nó trả về một Promise. Cái Promise này giống như một "biên lai" hay một "giấy cam kết" rằng: "Tao hứa sẽ trả về kết quả cho mày, hoặc báo lỗi nếu có vấn đề. Mày cứ đi làm việc khác đi, khi nào tao xong tao sẽ báo."
Một Promise có 3 trạng thái:
- Pending (Đang chờ): Giống như bạn vừa đặt đồ ăn, shipper đang trên đường. Lời hứa đang chờ được thực hiện.
- Fulfilled (Hoàn thành / Resolved): Đồ ăn đã đến, bạn đã nhận được kết quả mong muốn. Lời hứa đã được giữ.
- Rejected (Thất bại): Shipper bom hàng, đồ ăn bị lỗi, hoặc có lỗi xảy ra trong quá trình thực hiện. Lời hứa bị phá vỡ.
2. Code Ví Dụ Minh Họa: "Lời Hứa" Của Anh Shipper
Để các bạn hình dung rõ hơn về cách tạo và sử dụng Promise, chúng ta hãy cùng xem xét ví dụ về một anh shipper giao hàng nhé.
// Hàm mô phỏng việc giao hàng bất đồng bộ
function giaoHang(tenMonAn, thoiGianGiao) {
return new Promise((resolve, reject) => {
console.log(`Anh shipper đang chuẩn bị giao món "${tenMonAn}"...`);
// Mô phỏng thời gian giao hàng
setTimeout(() => {
const randomSuccess = Math.random() > 0.3; // 70% thành công, 30% thất bại
if (randomSuccess) {
// Giao hàng thành công
resolve(`Chúc mừng! Món "${tenMonAn}" đã được giao thành công sau ${thoiGianGiao / 1000} giây.`);
} else {
// Giao hàng thất bại
reject(`Ôi không! Món "${tenMonAn}" bị bom hàng hoặc gặp sự cố trên đường đi.`);
}
}, thoiGianGiao);
});
}
// Sử dụng Promise
console.log("--- Bắt đầu đặt hàng ---");
giaoHang("Trà Sữa Trân Châu Đường Đen", 2000) // Đặt món 1
.then((ketQua) => {
// Khi lời hứa được "resolve" (thành công)
console.log("Thành công: " + ketQua);
return giaoHang("Bánh Tráng Trộn Cô Tư", 1500); // Đặt món 2, chuỗi Promise
})
.then((ketQuaMon2) => {
console.log("Thành công: " + ketQuaMon2);
return giaoHang("Bún Đậu Mắm Tôm Đặc Biệt", 3000); // Đặt món 3
})
.catch((loi) => {
// Khi lời hứa bị "reject" (thất bại) ở bất kỳ bước nào
console.error("Thất bại: " + loi);
})
.finally(() => {
// Luôn chạy dù thành công hay thất bại
console.log("--- Kết thúc quá trình đặt hàng ---");
console.log("Cảm ơn quý khách đã sử dụng dịch vụ!");
});
console.log("--- Quý khách có thể lướt TikTok trong khi chờ đợi... ---");
Trong ví dụ trên:
new Promise((resolve, reject) => { ... }): Đây là cách bạn tạo một Promise. Bạn truyền vào một hàm thực thi (executor function) với hai đối số:resolve(gọi khi thành công) vàreject(gọi khi thất bại)..then((ketQua) => { ... }): Phương thức này được gọi khi Promise đượcresolve. Nó nhận kết quả từresolvevà bạn có thể xử lý nó. Quan trọng hơn,.then()cũng trả về một Promise mới, cho phép bạn xâu chuỗi (chaining) nhiều thao tác bất đồng bộ liên tiếp, tránh được Callback Hell..catch((loi) => { ... }): Phương thức này được gọi khi Promise bịreject. Nó giúp bạn xử lý lỗi một cách tập trung, thay vì phải kiểm tra lỗi ở từng Callback..finally(() => { ... }): Phương thức này luôn được gọi, dù Promise thành công hay thất bại. Rất hữu ích cho các tác vụ dọn dẹp (cleanup) như đóng kết nối, ngừng spinner loading.

3. Mẹo Vặt (Best Practices) "Đỉnh Cao" từ Giảng Viên Creyt
Để dùng Promises "chất" như dân chuyên, nhớ mấy mẹo này nhé:
- Luôn có
.catch(): Giống như đi xe máy phải đội mũ bảo hiểm vậy. Nếu một Promise bịrejectmà không có.catch(), chương trình của bạn có thể bị crash (unhandled promise rejection). Luôn luôn có một.catch()ở cuối chuỗi Promise để xử lý lỗi tổng thể. - Return Promises để Chaining: Muốn xâu chuỗi nhiều thao tác bất đồng bộ (như ví dụ giao hàng ở trên), hãy đảm bảo rằng mỗi
.then()trả về một Promise mới. Điều này giúp code của bạn gọn gàng và dễ đọc hơn rất nhiều. - Sử dụng
async/await(The Game Changer): Nếu Promises là "lời hứa", thìasync/awaitchính là "người hùng đến sau" giúp bạn viết code bất đồng bộ trông giống như code đồng bộ. Nó là syntactic sugar (cú pháp đường) trên Promises, giúp bạn chờ đợi kết quả của Promise một cách trực quan hơn. Anh Creyt sẽ có một bài riêng vềasync/awaitsau, nhưng hãy biết rằng nó là tương lai! Promise.all()cho các nhiệm vụ độc lập: Khi bạn có nhiều tác vụ bất đồng bộ không phụ thuộc vào nhau và bạn muốn chờ tất cả chúng hoàn thành, hãy dùngPromise.all(). Ví dụ: tải 3 ảnh cùng lúc. Nếu một trong số đó thất bại,Promise.all()sẽrejectngay lập tức.Promise.allSettled()cho các nhiệm vụ độc lập (không quan tâm thành bại): Tương tựPromise.all(), nhưng nó sẽ chờ tất cả các Promise hoàn thành, dù thành công hay thất bại, và trả về một mảng kết quả mô tả trạng thái của từng Promise. Hữu ích khi bạn muốn thực hiện nhiều tác vụ và thu thập kết quả của tất cả, bất kể có lỗi xảy ra với một vài tác vụ trong số đó.Promise.race()cho nhiệm vụ "ai nhanh hơn": Khi bạn muốn chờ đợi Promise nào hoàn thành (resolve hoặc reject) đầu tiên, hãy dùngPromise.race(). Ví dụ: gửi request đến nhiều server và lấy kết quả từ server phản hồi nhanh nhất.
4. Ứng Dụng Thực Tế "Đỉnh Của Chóp"
Promises đã trở thành xương sống của lập trình bất đồng bộ hiện đại. Bạn có thể thấy nó ở khắp mọi nơi:
- Gọi API: Hầu hết các thư viện HTTP client như
axioshayfetch(trong trình duyệt và Node.js từ v18) đều trả về Promises. Khi bạnfetch('https://api.example.com/data'), bạn nhận về một Promise. - Thao tác Database: Các ORM/ODM phổ biến như Mongoose (MongoDB) hay Sequelize (SQL) trong Node.js đều sử dụng Promises để xử lý các truy vấn database bất đồng bộ.
- Đọc/Ghi File: Module
fs.promisestrong Node.js cung cấp các phiên bản Promise-based của các hàm đọc/ghi file, giúp bạn quản lý các thao tác I/O dễ dàng hơn. - Animation và UI Updates: Trong lập trình giao diện người dùng (frontend), Promises được dùng để điều phối các animation phức tạp hoặc cập nhật UI sau khi một tác vụ dài hơi hoàn thành.
5. Nên Dùng Promises cho Case Nào?
Thử nghiệm và kinh nghiệm của anh Creyt cho thấy, bạn nên dùng Promises (hoặc async/await là cách dùng Promise "thanh lịch" nhất) cho mọi tác vụ bất đồng bộ mà bạn gặp phải.
- Khi bạn cần thực hiện một chuỗi các thao tác bất đồng bộ tuần tự: Ví dụ: "Đọc file A -> Xử lý dữ liệu A -> Ghi vào database -> Gửi email xác nhận". Promises chaining là lựa chọn hoàn hảo.
- Khi bạn cần xử lý lỗi tập trung: Thay vì phải kiểm tra lỗi ở từng bước trong Callback Hell,
.catch()giúp bạn gom tất cả lỗi về một mối. - Khi bạn muốn code của mình dễ đọc, dễ bảo trì: Promises biến code bất đồng bộ trở nên dễ hiểu hơn, giống như đọc một câu chuyện tuần tự.
- Khi bạn cần quản lý nhiều tác vụ bất đồng bộ độc lập:
Promise.all()hayPromise.allSettled()sẽ là người bạn đồng hành đắc lực.
Tóm lại, Promises không chỉ là một khái niệm, nó là một tư duy mới để quản lý sự phức tạp của thế giới bất đồng bộ. Hãy làm quen và "kết thân" với nó, code của bạn sẽ lên một tầm cao mới!
Thuộc Series: Nodejs
Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!