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?
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
showParking()chạy tớiawaitđầ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
- 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
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.
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
}
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.
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)
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ống | Cách viết | Lý do |
|---|---|---|
| Lấy số lượng tồn kho, số lượt review và ngày giao hàng dự kiến | await 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ận | Mỗ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 |
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
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 kiến thức
Hãy trả lời từng câu hỏi một.
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ì?