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

async / await — Chờ kết quả Promise theo từng dòng từ trên xuống

Dùng async / await để chờ kết quả Promise, bắt lỗi bằng try / catch, so sánh chờ tuần tự với Promise.all và xem điều gì xảy ra khi quên await.

Với then của Promise, bạn chỉ nhận được giá trị qua tham số của một hàm. Muốn dùng kết quả đầu tiên ở bước thứ ba, bạn phải lồng hàm vào nhau hoặc chép giá trị ra một biến bên ngoài, và code trở nên khó đọc.

Bài này nói về async / await, cách viết chờ có kết quả rồi mới sang dòng tiếp theo, cùng các cách chờ nhiều Promise.

Viết các bước từ trên xuống — Hàm async và await

Giả sử bạn giữ một chỗ trống trong bãi đỗ xe và đưa số chỗ được cấp vào dòng thông báo. Nếu viết bằng then, mọi xử lý dùng đến số chỗ đều phải nằm trong hàm truyền cho then, còn code viết ở các dòng bên dưới, ngoài hàm đó, sẽ chạy trước khi số chỗ được trả về.

Bên trong hàm async (hàm được khai báo kèm async, khi gọi luôn trả về một Promise), bạn dùng được await. await chỉ tạm dừng phần còn lại của hàm đó cho đến khi có kết quả, rồi lấy ra giá trị mà Promise được fulfilled. Trong lúc hàm đang dừng, phía gọi nhận ngay một Promise ở trạng thái pending.

// Trả về Promise được fulfilled với value sau ms mili giây (giống bài trước)
function delay(ms, value) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(value), ms);
  });
}
const assignSpot = () => delay(100, "B2-17");

// Viết await bên trong hàm có async
async function showParking() {
  console.log("Đang tìm chỗ trống");          // Đang tìm chỗ trống
  const spot = await assignSpot();            // Chưa fulfilled thì chưa sang dòng tiếp theo
  console.log(`Đã giữ chỗ ${spot}`);          // Đã giữ chỗ B2-17
  return `Bạn có thể đỗ xe ở chỗ ${spot}`;    // Promise đã trả về trước đó được fulfilled với giá trị này
}

// Phía gọi cũng dùng await để chờ Promise được trả về
const message = await showParking();
console.log(message);                         // Bạn có thể đỗ xe ở chỗ B2-17
Mỗi await đang chờ điều gì
Phía gọi — const message = await showParking()
  • showParking() chạy tới await đầu tiên bên trong thì trả về một Promise ở trạng thái pending
  • Chưa tới console.log(message) cho đến khi Promise đó fulfilled
Bên trong async function showParking()
  • In ra Đang tìm chỗ trống
  • await assignSpot() — dừng cho đến khi fulfilled sau 100ms
  • Sang dòng tiếp theo khi spot đã chứa "B2-17"
  • Promise đã trả về được fulfilled với dòng chữ được return
await bên trong chờ kết quả của assignSpot, await bên ngoài chờ hàm chạy xong. Giá trị được return sẽ nằm trong message.

Hàm async không có return vẫn trả về một Promise, và Promise đó được fulfilled với undefined khi thân hàm chạy đến cuối. Kể cả khi không cần nhận giá trị, nếu thêm await vào lời gọi thì dòng tiếp theo sẽ chờ đến khi xử lý bên trong xong.

await viết ngoài hàm chỉ dùng được ở một số nơi

Môi trường chạy trên trang này bọc toàn bộ code của bạn trong một hàm async, nên bạn viết được await cả ở ngoài hàm, như const message = await showParking();. Trong một file JavaScript bình thường, await đặt ngoài hàm async có thể gây SyntaxError, vì vậy khi viết code ở nơi khác, hãy đặt await bên trong hàm async.

Gửi thông báo đặt lịch cho một tiệm làm tóc. delay, confirmBooking và registerReminder đã được khai báo sẵn.

① Hãy viết lại notifyBooking thành một hàm có thể chờ kết quả bên trong.

② Hãy chờ ngày giờ được xác nhận, rồi in ra "Lịch hẹn của bạn: 15:00 ngày 14/9".

③ Hãy đăng ký nhắc lịch với ngày giờ đó, rồi in ra kết quả.

④ Ở bên ngoài hàm, hãy chờ hàm chạy xong rồi in ra "Đã hoàn tất đặt lịch".

(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 như một ngoại lệ — await và try / catch

Khi bạn giữ một ghế xem hòa nhạc rồi thanh toán bằng thẻ, việc thanh toán có thể thất bại. Cách lấy giá trị bằng await không tạo thành chuỗi then, nên bạn không thể bắt lỗi bằng .catch ở cuối chuỗi như bài trước.

Nếu throw bên trong một hàm async, Promise mà hàm đó trả về sẽ bị rejected. Khi bạn await một Promise bị rejected, chính Error đó được ném ra ngay tại dòng await, nên bạn chỉ cần bọc nó trong try / catch.

// delay giống code ở phần đầu tiên
const holdSeat = () => delay(100, "S-204");
const sendReceipt = (seatId) => delay(100, `Đã gửi hóa đơn cho ghế ${seatId}`);

// throw bên trong hàm async thì Promise nó trả về bị rejected
async function payByCard() {
  await delay(100);                          // Không dùng giá trị, chỉ chờ 100ms
  throw new Error("Thẻ đã hết hạn");
}

try {
  const seatId = await holdSeat();
  console.log(`Đã giữ ghế ${seatId}`);                    // Đã giữ ghế S-204
  await payByCard();                                      // Bị rejected nên dòng này ném ngoại lệ
  const receipt = await sendReceipt(seatId);              // Từ đây trở xuống không chạy
  console.log(receipt);
} catch (error) {
  console.log(`Không mua được vé: ${error.message}`);     // Không mua được vé: Thẻ đã hết hạn
}
Dòng nào chạy tiếp khi thành công và khi thất bại
Nếu thanh toánthành côngawait payByCard()chạy xongSang sendReceiptở dòng kếGửihóa đơnNếu thẻđã hết hạnawait payByCard()ném ngoại lệBỏ qua phầncòn lại của trycatch (error)in ra thông báo
Hàng trên đi tiếp sang dòng kế, hàng dưới bỏ qua phần còn lại của try và nhảy sang catch. Đi đâu tiếp là do trạng thái của Promise được await quyết định.

catch chỉ nhận các ngoại lệ xảy ra bên trong cặp ngoặc nhọn của try, vì vậy hãy đặt những dòng await có thể thất bại vào trong try. Việc dọn dẹp cần làm dù thành công hay thất bại thì viết vào khối finally sau try / catch.

Gia hạn những cuốn sách đã mượn ở thư viện. delay, loans và renewLoan đã được khai báo sẵn.

① Trong requestRenewal, hãy chờ kết quả của renewLoan và in ra thông báo nhận được.

② Nếu thất bại, hãy in ra "Không gia hạn được: " kèm theo message của Error.

③ Dù thành công hay thất bại, cuối cùng hãy in ra một dòng như "Đã xử lý xong Nhập môn thống kê".

④ Hãy chờ cuốn đầu tiên trong loans xử lý xong rồi mới gửi yêu cầu cho cuốn thứ hai.

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

Bắt đầu ba lời gọi cùng lúc — Tuần tự và Promise.all

Giả sử khi mở trang sản phẩm, bạn cần lấy số lượng tồn kho, số lượt review và ngày giao hàng dự kiến. Nếu viết mỗi dòng một await, dù không dòng nào dùng kết quả của dòng trước, request sau vẫn phải chờ request trước xong mới bắt đầu. Những request chẳng liên quan gì đến nhau cũng phải xếp hàng chờ.

Cách chạy chờ việc trước xong rồi mới bắt đầu việc sau gọi là tuần tự, còn cách bắt đầu nhiều việc cùng lúc rồi mới chờ gọi là song song. Để chạy song song, bạn gọi cả ba hàm trước để lấy các Promise, truyền chúng vào Promise.all đã học ở bài trước, rồi chờ bằng await thay cho then.

// delay giống code ở phần đầu tiên
const loadStock = () => delay(120, 8);
const loadReviews = () => delay(50, 23);
const loadArrival = () => delay(90, "13/9");

// Tuần tự: có kết quả rồi mới gọi hàm kế tiếp (mất 120 + 50 + 90 = 260ms mới đủ)
const stock = await loadStock();
const reviews = await loadReviews();
const arrival = await loadArrival();
console.log([stock, reviews, arrival].join(" / "));  // 8 / 23 / 13/9

// Song song: gọi cả ba trước rồi mới chờ (đủ sau hàm chậm nhất, 120ms)
const promises = [loadStock(), loadReviews(), loadArrival()];
const results = await Promise.all(promises);
console.log(results.join(" / "));                   // 8 / 23 / 13/9 (kết quả giống nhau)
Thời điểm có kết quả: tuần tự và song song
Tuần tự 0msgọi tồn kho120ms có tồn khogọi review170ms có reviewgọi ngày giao260msđủ cả 3Song song 0msgọi cả 350mscó review90mscó ngày giao120ms có tồn khođủ cả 3
Ở hàng dưới, cả ba bắt đầu lúc 0ms và đủ kết quả lúc 120ms, theo hàm chậm nhất. Nếu viết mỗi dòng một await, tổng thời gian chờ là 260ms.

Nếu viết await trong thân for...of, các phần tử của mảng cũng được xử lý tuần tự từng cái một, và tổng thời gian chờ là tổng của mọi phần tử. Bảng dưới đây cho biết khi nào nên chọn tuần tự, khi nào nên chọn song song.

Tình huốngCách viếtLý do
Lấy số lượng tồn kho, số lượt review và ngày giao hàng dự kiếnawait Promise.all (song song)Không cái nào dùng kết quả của cái khác, nên có thể bắt đầu cùng lúc
Nhận mã đơn hàng rồi mới gửi email xác nhậnMỗi dòng một await (tuần tự)Lời gọi thứ hai cần kết quả của lời gọi thứ nhất
Xử lý từng phần tử của mảng theo đúng thứ tựawait trong thân for...of (tuần tự)Nếu chạy song song, các phần tử có thể xong theo thứ tự khác với thứ tự trong mảng

Một ứng dụng thời tiết tải dự báo cho ba thành phố. Hãy so sánh thứ tự các dự báo được trả về. delay, loadForecast và cities đã được khai báo sẵn.

① Hãy duyệt cities theo thứ tự, chờ có dự báo của một thành phố rồi mới tải thành phố tiếp theo.

② Hãy in ra "Đã tải lần lượt từng thành phố".

③ Lần này, hãy bắt đầu tải cả ba thành phố cùng lúc và chờ đến khi có đủ.

④ Hãy in ra các dự báo, nối với nhau bằng " / ".

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

Gọi mà không có await — Biến nhận về một Promise

Giả sử bạn hiển thị số điểm tích lũy của thành viên và giới thiệu ưu đãi nếu họ có từ 1000 điểm trở lên. Nếu quên await khi gọi hàm async, sẽ không có SyntaxError nào cả — bạn chỉ nhận được dòng chữ sai và kết quả so sánh sai, nên rất khó nhận ra mình sai ở đâu.

Hàm async không trả về giá trị bạn return, mà trả về một Promise được fulfilled với giá trị đó. Nếu nhận kết quả mà không có await, biến sẽ chứa Promise, và khi nhúng nó vào template literal thì màn hình in ra [object Promise] (dạng chuỗi của một Promise).

// delay giống code ở phần đầu tiên
async function loadPoints() {
  await delay(100);
  return 1200;                              // Promise đã trả về trước đó được fulfilled với 1200
}

// Quên await thì points chứa một Promise
const points = loadPoints();
console.log(`Số điểm: ${points}`);          // Số điểm: [object Promise]
console.log(points >= 1000);                // false (Promise đổi sang số thành NaN nên phép so sánh sai)

// Có await thì biến chứa giá trị fulfilled
const balance = await loadPoints();
console.log(`Số điểm: ${balance}`);         // Số điểm: 1200
console.log(balance >= 1000);               // true
points vẫn là một Promise
points =loadPoints()Chứa một Promiseđang pendingFulfilledsau 100mspoints vẫnchứa PromiseNhúngvào chuỗiIn ra[object Promise]
Kể cả sau khi fulfilled, points cũng không bao giờ được thay bằng 1200. Muốn lấy giá trị thì cần await.

Nếu màn hình hiện [object Promise], hoặc một điều kiện lẽ ra phải đúng lại ra false, hãy kiểm tra xem lời gọi hàm async đã có await chưa. Khi đặt trong điều kiện của if, Promise là một object nên luôn truthy, và điều kiện luôn đúng bất kể hàm trả về giá trị gì.

Lỗi khi quên await không đến được catch

Bên trong try, nếu bạn gọi một hàm async trả về Promise bị rejected mà không có await, dòng đó không ném ngoại lệ và catch không bao giờ chạy. Không có chỗ nào xử lý lỗi, và môi trường chạy trên trang này cũng không in ra gì cả. Muốn nhận lỗi ở catch, hãy thêm await vào lời gọi.

Kiểm tra xem các coupon đã hết hạn chưa và cho người dùng biết coupon nào dùng được. delay, coupons và isExpired đã được khai báo sẵn.

① Không dùng await, hãy gọi isExpired với coupon đầu tiên và dùng kết quả làm điều kiện của if.

② Nếu điều kiện đúng, hãy in ra "AUTUMN10 đã hết hạn"; nếu không, hãy in ra "AUTUMN10 dùng được".

③ Hãy duyệt coupons theo thứ tự, chờ từng kết quả rồi kiểm tra như trên và in ra thông báo.

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 1Bên trong try, các dòng nằm dưới dòng await một Promise bị rejected sẽ ra sao?

Câu 2Nếu viết mỗi dòng một await cho ba lời gọi lần lượt mất 70ms, 30ms và 50ms, thì sau khoảng bao lâu mới có đủ cả ba kết quả?

Câu 3Nếu gọi một hàm async kết thúc bằng return 1200; mà không có await rồi gán kết quả vào biến, biến đó sẽ chứa gì?