Có những việc không xong ngay: tải file level từ server, đọc dữ liệu từ mạng. JavaScript không đứng chờ những việc đó, vì chờ thì cả trang đứng hình. Nó dùng Promise để hẹn "xong thì báo", và async/await để bạn viết code chờ đợi mà vẫn đọc từ trên xuống như bình thường.
Vấn đề: dữ liệu chưa về
Người mới hay viết code tải dữ liệu như thể nó có ngay:
const response = fetch("level1.json");
console.log(response); // Promise { <pending> }
fetch không trả về dữ liệu. Nó trả về một Promise, tức một tấm vé hẹn: "dữ liệu đang tải, lát nữa có". Đọc response.enemies lúc này chỉ ra undefined.
Promise
Promise có ba trạng thái:
- pending: đang chờ.
- fulfilled: xong, có kết quả.
- rejected: thất bại, có lỗi.
Tự tạo một Promise để thấy rõ, ví dụ đếm ngược trước khi vào trận:
function wait(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
wait(1000) trả về Promise sẽ chuyển sang fulfilled sau 1 giây. Bạn ít khi phải tự viết Promise. Phần lớn thời gian bạn nhận Promise từ hàm có sẵn như fetch, và việc của bạn là chờ nó.
async và await
await đặt trước một Promise, nghĩa là "dừng hàm này ở đây, chờ Promise xong, lấy kết quả". await chỉ dùng được trong hàm có chữ async phía trước (hoặc ở cấp ngoài cùng của module).
function wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function countdown() {
console.log("3");
await wait(1000);
console.log("2");
await wait(1000);
console.log("1");
await wait(1000);
console.log("Chiến!");
}
countdown();
console.log("Đang tải bản đồ...");
// 3
// Đang tải bản đồ...
// 2
// 1
// Chiến!
Để ý thứ tự in. countdown in "3" rồi gặp await, tạm dừng. Trong lúc đó code bên ngoài chạy tiếp và in "Đang tải bản đồ...". Chỉ hàm async chờ, phần còn lại của trang không bị chặn.
Hàm async luôn trả về một Promise. Muốn lấy giá trị nó return thì cũng phải await.
Lỗi hay gặp: dùng
awaittrong hàm thường. JS báoSyntaxError: await is only valid in async functions and the top level bodies of modules. Thêmasynctrướcfunctionlà xong.
fetch một file JSON
Giả sử cạnh index.html có file level1.json:
{ "name": "Rừng tối", "enemies": ["Slime", "Goblin"], "timeLimit": 90 }
Tải và dùng nó:
async function loadLevel(file) {
const response = await fetch(file);
const level = await response.json();
return level;
}
const level = await loadLevel("level1.json");
console.log(level.name); // Rừng tối
console.log(level.enemies.length); // 2
Có hai lần await. Lần đầu chờ server trả lời. Lần hai chờ đọc hết nội dung và đổi từ JSON sang object. response.json() cũng trả về Promise.
Dòng await loadLevel(...) ở ngoài hàm chỉ chạy được trong module (<script type="module">). Trong script thường thì gọi bên trong một hàm async khác.
fetch cần trang chạy qua một server. Mở file index.html bằng cách nhấp đúp (địa chỉ bắt đầu bằng file://) thì trình duyệt chặn fetch. Dùng tiện ích Live Server của VS Code, hoặc chạy npx serve trong thư mục dự án.
Bắt lỗi với try/catch
Mạng có thể rớt, file có thể sai tên. Promise bị rejected thì await ném ra lỗi, và không bắt thì hàm dừng giữa chừng. Bọc trong try...catch:
async function loadLevel(file) {
try {
const response = await fetch(file);
if (!response.ok) {
throw new Error(`Không tải được ${file}: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error(error.message);
return null;
}
}
const level = await loadLevel("level99.json");
// Không tải được level99.json: 404
console.log(level); // null
Chỗ dễ sót: fetch không coi lỗi 404 là thất bại. Server trả lời "không có file" vẫn là một câu trả lời, nên Promise vẫn fulfilled. Phải tự kiểm tra response.ok và tự throw lỗi.
Code trong try chạy bình thường. Dòng nào ném lỗi thì nhảy thẳng xuống catch, bỏ qua các dòng còn lại trong try. Biến error chứa thông tin lỗi, error.message là câu mô tả.
Bài tập
Viết hàm async tên startGame(): tải config.json (có dạng { "startHp": 100 }), in Bắt đầu với 100 máu. Nếu tải lỗi thì dùng máu mặc định 50 và in Dùng cấu hình mặc định.
Xem đáp án
async function startGame() {
let startHp = 50;
try {
const response = await fetch("config.json");
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const config = await response.json();
startHp = config.startHp;
} catch {
console.log("Dùng cấu hình mặc định");
}
console.log(`Bắt đầu với ${startHp} máu`);
}
startGame();
// có file: Bắt đầu với 100 máu