Promise là chỗ giữ chỗ cho một giá trị tương lai
Khi JavaScript cần làm việc gì đó tốn thời gian, như gửi request mạng, đọc tệp hay chờ một bộ hẹn giờ, nó không thể đưa ngay kết quả cho bạn. Thay vào đó, nó đưa bạn một Promise: một đối tượng đại diện cho giá trị sẽ có, vào một lúc nào đó.
console.log đầu tiên cho thấy một Promise đang pending. Nửa giây sau, Promise resolve và callback .then chạy với giá trị đó. Bản thân Promise chỉ là một đối tượng; điều kỳ diệu là nó biết cách báo cho bất kỳ ai đang lắng nghe khi giá trị đến.
Ba trạng thái
Một Promise luôn ở một trong ba trạng thái:
- pending: công việc đang diễn ra, chưa có giá trị.
- fulfilled: công việc thành công, đã có giá trị.
- rejected: công việc thất bại, đã có lỗi.
Một Promise chuyển từ pending sang fulfilled hoặc rejected đúng một lần, rồi giữ nguyên mãi mãi. Bạn không thể hủy resolve một Promise hay resolve nó hai lần.
Promise.resolve(value) tạo một Promise đã fulfilled sẵn; Promise.reject(error) tạo một Promise đã rejected sẵn. Rất tiện khi viết test và khi một hàm trả về Promise nhưng đôi khi có kết quả ngay lập tức.
Đọc giá trị: .then và .catch
Bạn không mở Promise ra trực tiếp mà truyền một callback cho .then, và Promise sẽ gọi nó khi giá trị sẵn sàng:
.catch(fn) chạy nếu Promise bị reject. Bên dưới, nó là cách viết tắt của .then(undefined, fn). Một .catch() ở cuối chuỗi xử lý lỗi reject từ bất kỳ bước nào phía trên, bạn không cần đặt nó sau mỗi .then.
Nối chuỗi: mỗi .then trả về một Promise mới
Đây là phần hay làm người học vấp. .then() không chỉ chạy một callback: nó trả về một Promise mới, resolve với bất cứ thứ gì callback trả về. Nhờ vậy bạn có thể nối chuỗi:
Mỗi bước đưa kết quả cho bước sau. Nếu callback của .then trả về một Promise, chuỗi sẽ chờ Promise đó xong rồi mới đi tiếp, nên các bước bất đồng bộ ghép với nhau rất gọn:
Ba bước bất đồng bộ nối tiếp nhau, không lồng nhau. So với cùng logic đó viết bằng callback, bạn sẽ hiểu vì sao Promise trở nên phổ biến.
Lỗi trôi xuống dọc theo chuỗi
Một Promise bị reject sẽ bỏ qua mọi .then cho tới khi gặp .catch. Đó là toàn bộ mô hình xử lý lỗi:
Ném lỗi bên trong .then sẽ reject Promise mà .then đó trả về. .then kế tiếp thấy lỗi reject và chuyển tiếp nó, cho tới khi .catch nuốt lấy. Thường bạn chỉ cần một .catch ở cuối chuỗi, còn một chuỗi hoàn toàn không có .catch sẽ sinh ra cảnh báo "unhandled promise rejection", điều bạn nên sửa.
Tự tạo Promise với new Promise
Phần lớn thời gian bạn sẽ dùng Promise do thư viện trả về. Thỉnh thoảng bạn cần bọc một thứ không trả về Promise, thường là API kiểu callback cũ:
Hàm bạn truyền vào new Promise được gọi là executor. Nó nhận hai đối số (argument): resolve (gọi với giá trị thành công) và reject (gọi với lỗi). Chỉ gọi một trong hai, đúng một lần. Sau đó, mọi lần gọi tiếp theo đều bị bỏ qua.
Hai thói quen giúp bạn đỡ khổ:
- Chỉ dùng
new Promisekhi bọc thứ chưa dựa trên Promise. Nếu một hàm đã trả về Promise, cứ trả về chính Promise đó. - Luôn
rejectvới một đối tượngError, không phải chuỗi. Stack trace rất đáng giữ lại.
Chạy song song: Promise.all
Chuỗi .then chạy tuần tự. Khi bạn có vài tác vụ bất đồng bộ độc lập và muốn chúng chạy cùng lúc, Promise.all là công cụ phù hợp:
Cả ba bộ hẹn giờ chạy đồng thời. Promise.all resolve với một mảng kết quả theo đúng thứ tự đầu vào, khi mọi Promise đã fulfilled. Tổng thời gian vào khoảng 400ms, không phải 900ms.
Điểm cần lưu ý: Promise.all reject ngay khi bất kỳ Promise nào bị reject, và các kết quả còn lại bị mất. Đó là hành vi đúng khi bạn cần đủ mọi phần (ví dụ hiển thị một trang cần ba lần gọi API). Khi không cần như vậy, hãy dùng allSettled.
Khi chấp nhận được một vài lỗi: Promise.allSettled
Promise.allSettled chờ mọi Promise kết thúc, dù fulfilled hay rejected, rồi đưa bạn một bản báo cáo:
Mỗi kết quả là một đối tượng: { status: "fulfilled", value } hoặc { status: "rejected", reason }. Hữu ích khi thành công một phần vẫn chấp nhận được: ghi log một loạt sự kiện, tải nhiều ảnh thu nhỏ, chạy các bước kiểm tra tình trạng độc lập.
Hai hàm kết hợp khác nên biết:
Promise.race([...]): kết thúc ngay khi Promise đầu tiên kết thúc, dù thành công hay thất bại. Tiện để đặt thời gian chờ tối đa.Promise.any([...]): fulfilled với kết quả thành công đầu tiên và bỏ qua các lỗi reject. Chỉ reject khi mọi Promise đều bị reject.
Promise luôn bất đồng bộ
Ngay cả một Promise đã resolve cũng gọi callback .then theo cách bất đồng bộ, không bao giờ đồng bộ, không bao giờ trong cùng một tick:
Kết quả là before, after, immediate. Callback .then chờ code hiện tại chạy xong rồi mới chạy trên hàng đợi microtask. Quy tắc "callback của Promise không bao giờ chạy đồng bộ" là lý do việc trộn Promise với code đồng bộ luôn dễ đoán: code đồng bộ luôn xong trước.
Tiếp theo: async/await
Nối chuỗi .then vẫn chạy tốt, nhưng khi có hơn hai, ba bước thì code bắt đầu trông như cầu thang. async/await là cú pháp bên trên Promise, cho bạn viết cùng logic như thể nó đồng bộ, dùng try/catch để bắt lỗi và biến thông thường cho các giá trị trung gian. Đó là nội dung tiếp theo.
Câu hỏi thường gặp
Promise trong JavaScript là gì?
Promise là một đối tượng đại diện cho một giá trị chưa sẵn sàng, thường là kết quả trong tương lai của một thao tác bất đồng bộ như request mạng. Nó luôn ở một trong ba trạng thái: pending, fulfilled hoặc rejected. Bạn đọc giá trị cuối cùng bằng cách gắn callback qua .then() và .catch().
then và catch khác nhau thế nào?
.then(onFulfilled) chạy khi Promise resolve thành công và nhận giá trị đã resolve. .catch(onRejected) chạy khi Promise đó (hoặc bất kỳ Promise nào trước nó trong chuỗi) bị reject và nhận lỗi. Chỉ cần một .catch() ở cuối chuỗi là xử lý được lỗi từ mọi bước phía trên.
Promise.all dùng để làm gì?
Promise.all([p1, p2, p3]) nhận một mảng Promise và trả về một Promise duy nhất, fulfilled với mảng chứa mọi giá trị đã resolve, nhưng chỉ khi mọi Promise đầu vào đều đã fulfilled. Chỉ cần một Promise bị reject là cả nhóm bị reject ngay lập tức. Dùng Promise.allSettled khi bạn muốn có mọi kết quả bất kể có lỗi hay không.
Nên dùng Promise hay async/await?
Hai thứ dùng chung một cơ chế: async/await là cú pháp bên trên Promise. Phần lớn code mới dễ đọc hơn với async/await, nhưng bạn vẫn trả về Promise, vẫn bắt lỗi bằng try/catch hoặc .catch(), và vẫn dùng Promise.all để chạy song song. Hiểu Promise hoạt động thế nào thì async/await sẽ trở nên dễ hiểu.