Học bằng cách đọc theo thứ tự

Promise — Nhận kết quả bất đồng bộ bằng then và catch

Nhận kết quả của một xử lý mất thời gian vào lúc sau bằng Promise: ba trạng thái, then / catch / finally, chuỗi then và Promise.all.

Có những xử lý mất một lúc mới có kết quả, như gửi đơn hàng hay kiểm tra tồn kho. Để trang không bị đứng trong lúc chờ, bạn viết những đoạn code này sao cho chương trình chạy ngay sang dòng tiếp theo mà không đợi, rồi nhận kết quả vào lúc sau. Cách viết này gọi là xử lý bất đồng bộ (asynchronous).

Bài này nói về Promise, thứ biểu thị một kết quả được xác định vào lúc sau, và Promise.all để chờ nhiều Promise cùng lúc.

Nhận kết quả vào lúc sau — Promise và then

Giả sử phải mất 100ms mới có câu trả lời về tình trạng còn chỗ ngồi. setTimeout (hàm gọi một hàm khác đúng một lần sau số mili giây chỉ định) trả về ngay lập tức và chương trình chạy tiếp sang dòng sau, nên bạn không thể return từ hàm của mình một câu trả lời đến sau 100ms.

Thay vào đó, bạn trả về một Promise (object biểu thị một kết quả được xác định vào lúc sau), và câu trả lời sẽ được đưa tới qua nó. Hàm bạn truyền cho new Promise((resolve) => { ... }) được gọi ngay lập tức, và khi gọi resolve(giá trị) bên trong, Promise chuyển từ pending (đang chờ kết quả) sang fulfilled (thành công). Hàm bạn truyền cho phương thức then sẽ được gọi với giá trị đó khi Promise đã fulfilled.

// Trả về một Promise sẽ fulfilled với value sau ms mili giây
function delay(ms, value) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(value), ms);
  });
}

// Bắt đầu kiểm tra (lúc này vẫn đang pending)
const seats = delay(100, "Còn 12 chỗ");

// Truyền trước hàm sẽ được gọi khi đã fulfilled
seats.then((message) => {
  console.log(message);                        // Còn 12 chỗ (hiển thị thứ 2)
});

console.log("Đang kiểm tra tình trạng chỗ ngồi");  // Đang kiểm tra tình trạng chỗ ngồi (hiển thị thứ 1)
Hàm truyền cho then chạy sau cùng
Gọidelay(100, …)Trả về Promiseđang pendingTruyền hàm choseats.then(…)Hàm đóchưa được gọiconsole.log ởdòng tiếp theoThông báo đangkiểm tra in trước100ms sau khigọi delayfulfilled; hàmđã truyền chạy
then chỉ giữ lấy hàm, không gọi nó ngay tại chỗ. Hàm bạn truyền vào chỉ chạy sau khi các dòng bên dưới đã chạy xong và Promise đã fulfilled.

Nếu viết đoạn code dùng kết quả ở bên ngoài then, nó sẽ chạy trước khi kết quả được xác định, nên chưa có giá trị để dùng. Hãy gom các dòng dùng kết quả vào trong hàm truyền cho then, còn những việc muốn làm trong lúc chờ thì viết ở các dòng phía sau, bên ngoài then.

Thêm một dòng chờ ở cuối để thấy kết quả của then

Trình chạy code trên trang này chỉ thu những gì được in ra trước khi dòng cuối cùng chạy xong. Trong trình duyệt hay Node.js, hàm của then vẫn chạy dù không có gì chờ nó, nhưng ở đây bạn thêm await delay(1000); vào cuối để chờ 1 giây. await (cú pháp không chạy sang dòng tiếp theo cho đến khi kết quả của Promise được xác định) sẽ được nói ở bài tiếp theo.

Hiển thị thông báo khi từng video trong 2 video được chuyển đổi xong. delay đã được khai báo sẵn.

① Trong convertVideo, hãy trả về một Promise sẽ fulfilled sau ms mili giây với thông báo theo dạng "Đã chuyển đổi xong intro.mp4".

② Hãy chuyển đổi intro.mp4 trong 150ms và outro.mp4 trong 80ms, rồi hiển thị thông báo khi nhận được.

③ Bên dưới ②, hãy hiển thị "Bạn vẫn chỉnh sửa được trong lúc chuyển đổi".

(Nếu chạy đúng, phần giải thích sẽ xuất hiện.)

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

Xử lý thất bại — reject / catch / finally

Việc xác minh mã PIN của thẻ sẽ thất bại nếu nhập sai số. Nếu chỉ có resolve, thất bại cũng tới dưới dạng một giá trị bình thường giống như thành công, nên hàm nhận kết quả lần nào cũng phải dùng if để phân biệt.

Hàm truyền cho new Promise còn nhận được reject ở tham số thứ hai. Khi gọi nó, như reject(new Error("nội dung")), Promise sẽ thành rejected (thất bại). Hàm truyền cho catch nhận Error đó khi Promise bị rejected, còn hàm truyền cho finally được gọi sau cùng, dù thành công hay thất bại.

function verifyPin(pin) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (pin === "4821") {
        resolve("Đã xác minh mã PIN");
      } else {
        // Báo thất bại bằng cách truyền một Error cho reject
        reject(new Error("Mã PIN không đúng"));
      }
    }, 100);
  });
}

// then, catch và finally có thể nối tiếp nhau (cách hoạt động được giải thích ở phần sau)
verifyPin("0000")
  .then((message) => console.log(message))        // không được gọi vì đã bị rejected
  .catch((error) => console.log(error.message))   // Mã PIN không đúng
  .finally(() => console.log("Đã xóa trống ô nhập"));
  // Đã xóa trống ô nhập (luôn in ra sau cùng, dù thành công hay thất bại)
resolve và reject quyết định trạng thái
Tạo bằng newPromise (pending)Truyền giá trịcho resolveTruyền Errorcho rejectKhông gọi cảresolve lẫn rejectChuyển thànhfulfilledChuyển thànhrejectedVẫn làpendingGọi then,rồi đến finallyGọi catch,rồi đến finallythen/catch/finallyđều không chạy
Ở hai cột bên trái, finally được gọi sau then hoặc catch. Hàm nào được gọi phụ thuộc vào việc resolve hay reject đã được gọi.

Khi Promise bị rejected, các hàm then nối trước catch sẽ bị bỏ qua, và hàm catch phía sau được gọi. catch này là phương thức của Promise, khác với câu lệnh try / catch. Nếu quên gọi cả resolve lẫn reject, code sẽ trông như bị treo, không có kết quả mà cũng không có lỗi.

Tra cứu số dư thẻ quà tặng. delay, balancescodes đã được khai báo sẵn.

① Trong checkGiftCard, nếu mã không bắt đầu bằng "GIFT-", hãy reject ngay mà không chờ, với một Error có nội dung "Mã sai định dạng".

② Các trường hợp còn lại, hãy resolve sau 100ms với thông báo theo dạng "Số dư: 3000 yên".

③ Hãy lần lượt tra cứu từng mã trong codes và hiển thị thông báo kết quả.

④ Dù thành công hay thất bại, hãy hiển thị "Đã ẩn chỉ báo đang tra cứu" ở cuối.

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

Chạy các bước theo thứ tự — chuỗi then và return

Giả sử sau khi đặt hàng, bạn muốn gửi email xác nhận bằng mã đơn hàng nhận về. Nếu viết yêu cầu thứ hai bên trong then đầu tiên, rồi yêu cầu thứ ba lại nằm trong đó, thì mỗi bước thêm vào code lại thụt lề sâu hơn và càng khó đọc.

then trả về một Promise mới, nên bạn nối được các lời gọi như .then(...).then(...). Cách viết này gọi là chuỗi (chain — nối then tiếp theo vào Promise mà then trước trả về). Giá trị bạn return từ một hàm then sẽ được hàm then tiếp theo nhận.

// delay giống với code ở phần đầu
const placeOrder = () => delay(100, "A-2046");
const sendMail = (orderId) => delay(100, `Đã gửi email xác nhận cho ${orderId}`);

placeOrder()
  .then((orderId) => {
    console.log(`Mã đơn hàng: ${orderId}`);   // Mã đơn hàng: A-2046
    return sendMail(orderId);                // chuỗi chờ đến khi email được gửi xong
  })
  .then((result) => {
    console.log(result);                     // Đã gửi email xác nhận cho A-2046
    return "Đã hoàn tất mọi bước";           // giá trị được chuyển thẳng cho hàm tiếp theo
  })
  .then((status) => console.log(status));    // Đã hoàn tất mọi bước
Không có return thì then không chờ
returnsendMail(orderId)Chờ đến khiemail gửi xongresult chứakết quả gửiBáo hoàn tấtsau khi gửiChỉ gọisendMail(orderId)then tiếp theochạy luônresult làundefinedBáo hoàn tấttrước khi gửi
Chỉ hàng trên chờ email gửi xong rồi mới gọi hàm tiếp theo. then tiếp theo chỉ chờ kết quả khi bạn return Promise.

Không có return thì hàm trả về undefined, nên then tiếp theo chạy luôn mà không chờ email được gửi, và resultundefined. Không có lỗi nào hiện ra, chỉ có kết quả gửi bị mất đi, nên đây là lỗi rất dễ bỏ sót.

Đã thêm ngoặc nhọn thì không được bỏ return

Arrow function không có ngoặc nhọn như .then((id) => sendMail(id)) trả về nguyên Promise của sendMail, nên then tiếp theo sẽ chờ. Nếu bạn bọc thân hàm trong ngoặc nhọn để chèn thêm một dòng log, hãy viết lại return, như return sendMail(id);.

Thanh toán bằng yên cho một món hàng giá 12 đô la ở cửa hàng nước ngoài. delay, dollars, loadRatechargeCard đã được khai báo sẵn.

① Hãy dùng tỷ giá để quy đổi dollars sang yên, hiển thị "Số tiền quy đổi: 1800 yên", rồi chuyển số tiền cho bước sau.

② Hãy thanh toán số tiền đó, và chỉ chạy tiếp khi kết quả đã được xác định.

③ Hãy hiển thị kết quả thanh toán.

④ Nếu thất bại ở bất kỳ bước nào, hãy hiển thị "Thanh toán thất bại: " kèm nội dung của Error.

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

Chờ nhiều kết quả cùng lúc — Promise.all

Giả sử khi mở trang quản trị, bạn gửi riêng từng yêu cầu lấy doanh thu hôm nay, số đơn hàng và số yêu cầu hỗ trợ, rồi hiển thị bảng tổng hợp khi đã có đủ cả ba. Nếu gắn then riêng cho từng Promise, thì Promise nào về sau cùng mỗi lần một khác, nên không có một chỗ cố định để kiểm tra rằng cả ba đã về đủ.

Nếu gọi ba hàm rồi truyền các Promise nhận được cho Promise.all (phương thức nhận một mảng Promise và chờ đến khi có đủ mọi kết quả), bạn sẽ nhận cả ba kết quả cùng lúc dưới dạng một mảng. Chỉ cần một Promise bị rejected, chương trình sẽ đi vào catch thay vì then.

// delay giống với code ở phần đầu
const loadSales = () => delay(100, 184000);
const loadOrders = () => delay(50, 42);
const loadInquiries = () => delay(80, 3);

// Cả ba bắt đầu ngay khi các hàm được gọi. Promise.all chờ đến khi có đủ
Promise.all([loadSales(), loadOrders(), loadInquiries()])
  // Nhận mảng kết quả bằng cách destructuring theo vị trí
  .then(([sales, orders, inquiries]) => {
    // Nhận theo thứ tự viết trong mảng, không phải thứ tự hoàn thành
    console.log(`Doanh thu: ${sales} yên`);          // Doanh thu: 184000 yên
    console.log(`Đơn hàng: ${orders}`);              // Đơn hàng: 42
    console.log(`Yêu cầu hỗ trợ: ${inquiries}`);     // Yêu cầu hỗ trợ: 3
  })
  .catch((error) => console.log(error.message));
Thứ tự truyền vào không phải thứ tự hoàn thành
Index 0loadSales()184000 ở 100msxong thứ 3Index 1loadOrders()42 ở 50msxong thứ 1Index 2loadInquiries()3 ở 80msxong thứ 2
Ba Promise lần lượt xong ở 50ms, 80ms và 100ms. then nhận [184000, 42, 3], theo thứ tự đã truyền vào (cột bên trái).

Bản thân Promise.all không bắt đầu yêu cầu nào; nó chỉ chờ đến khi có đủ kết quả. Bảng dưới đây tóm tắt, với từng trạng thái của các Promise truyền vào, Promise mà Promise.all trả về và hàm nào được gọi.

Trạng thái các Promise truyền vàoPromise do Promise.all trả vềHàm được gọi và giá trị nhận được
Tất cả đều fulfilledFulfilled khi Promise cuối cùng fulfilledthen nhận một mảng theo thứ tự đã truyền vào
Một Promise bị rejectedRejected ngay khi Promise đó bị rejectedcatch nhận Error đó
Từ hai Promise trở lên bị rejectedRejected ngay khi Promise đầu tiên bị rejectedcatch chỉ nhận Error đầu tiên
Các Promise khác đã fulfilled, nhưng còn một Promise vẫn pendingVẫn là pendingCả then lẫn catch đều không được gọi

Báo giá tổng cho một chuyến du lịch. Bốn hàm có tên bắt đầu bằng quote đã được khai báo sẵn.

① Trong estimateTrip, hãy chờ đến khi có đủ kết quả của mọi Promise nhận được.

② Hãy hiển thị tổng các mức giá theo dạng "Tổng: 68500 yên".

③ Nếu chỉ cần một báo giá thất bại, hãy hiển thị "Không báo giá được: " kèm nội dung của Error.

④ Hãy báo giá một lần với vé máy bay, khách sạn và xe thuê, rồi báo giá lại sau khi thay xe thuê bằng quoteFullCar (đã hết xe).

JavaScript / TypeScript Editor

Chạy code để xem đầu ra
QUIZ

Kiểm tra kiến thức

Hãy trả lời từng câu hỏi một.

Câu 1Nếu sau delay(100, "A").then((v) => console.log(v)); là dòng console.log("B");, kết quả sẽ hiển thị theo thứ tự nào?

Câu 2Nếu nối .then(A).catch(B).finally(C) vào một Promise bị rejected, những hàm nào được gọi?

Câu 3then của Promise.all([delay(100, "A"), delay(50, "B")]) nhận được mảng nào?