NỘI DUNG BÀI HỌC
-
🚀 Bắt đầu: Hiểu rõ vấn đề của code đồng bộ và tại sao cần bất đồng bộ.
-
🤝 Nền tảng: Làm chủ Promise và phương thức
.then(),.catch(). -
✨ Nâng cao: Tinh giản code với cú pháp
async/awaithiện đại. -
🔒 Bảo vệ: Viết code bền vững, chống sập với
try...catch...finally. -
🗂️ Tổ chức: Sắp xếp dự án chuyên nghiệp bằng Modules.
⏳ Phần 1: Vì sao JavaScript cần bất đồng bộ?
Trong thực tế, có rất nhiều việc không thể có kết quả ngay lập tức: gọi API, đọc file, chờ người dùng click, chờ trình duyệt tải trang hoặc chờ element xuất hiện. Nếu JavaScript cứ đứng yên và chặn toàn bộ chương trình để đợi từng việc xong, ứng dụng sẽ rất chậm và trải nghiệm sẽ tệ.
Đó là lý do JavaScript cần bất đồng bộ (asynchronous). Thay vì đứng chờ một việc hoàn tất rồi mới làm việc tiếp theo, chương trình có thể tiếp tục xử lý phần khác và quay lại khi kết quả đã sẵn sàng.
🔹 1. Đồng bộ và bất đồng bộ khác nhau thế nào?
- Đồng bộ: lệnh sau phải chờ lệnh trước chạy xong mới được chạy.
- Bất đồng bộ: có những tác vụ được giao đi xử lý, còn luồng chính vẫn tiếp tục làm việc khác.
💻 Ví dụ dễ hình dung:
console.log("Bước 1: Gửi yêu cầu tải dữ liệu");
setTimeout(() => {
console.log("Bước 2: Dữ liệu đã tải xong sau 2 giây"); // Chạy muộn hơn
}, 2000);
console.log("Bước 3: Chương trình vẫn tiếp tục chạy mà không đứng chờ");
Trong ví dụ trên, JavaScript không đứng chờ setTimeout() hoàn tất. Nó tiếp tục chạy dòng dưới ngay lập tức, rồi sau 2 giây mới quay lại thực thi callback.
🤝 Phần 2: Promise và cách xử lý với .then(), .catch(), .finally()
Promise là đối tượng đại diện cho kết quả trong tương lai của một tác vụ bất đồng bộ. Bạn có thể xem nó như một “lời hứa”: hoặc thành công, hoặc thất bại, nhưng kết quả chưa có ngay lúc này.
🔹 1. Promise có những trạng thái nào?
pending: đang chờ, chưa xong.fulfilled: hoàn thành thành công.rejected: thất bại.
🔹 2. Tạo và dùng một Promise cơ bản
Khi tự tạo Promise, bạn sẽ dùng resolve cho nhánh thành công và reject cho nhánh thất bại.
function taiDuLieu() {
return new Promise((resolve, reject) => {
console.log("Bắt đầu tải dữ liệu...");
setTimeout(() => {
const thanhCong = true; // Đổi thành false để xem nhánh catch hoạt động
if (thanhCong) {
resolve({ data: "Đây là dữ liệu người dùng" }); // Thành công => trả dữ liệu
} else {
reject("Lỗi mạng!"); // Thất bại => trả lý do lỗi
}
}, 2000);
});
}
taiDuLieu()
.then((response) => {
console.log("Tải thành công!", response.data); // Chạy khi Promise fulfilled
})
.catch((error) => {
console.error("Tải thất bại!", error); // Chạy khi Promise rejected
})
.finally(() => {
console.log("Hoàn tất xử lý Promise, dù thành công hay thất bại.");
});
console.log("Dòng này chạy ngay, không cần chờ tải xong.");
🔹 3. Promise chaining
Sức mạnh lớn của Promise là bạn có thể nối chuỗi nhiều bước bất đồng bộ lại với nhau bằng nhiều .then(). Mỗi .then() có thể trả về một Promise mới, và bước sau sẽ chờ bước trước hoàn tất.
function layUserId() {
return new Promise((resolve) => {
setTimeout(() => resolve(1), 1000); // Giả lập lấy userId
});
}
function layBaiVietCuaUser(userId) {
console.log(`Đang lấy bài viết cho user ID: ${userId}...`);
return new Promise((resolve) => {
setTimeout(() => resolve(["Bài viết 1", "Bài viết 2"]), 1000);
});
}
layUserId()
.then((userId) => {
return layBaiVietCuaUser(userId); // Trả về Promise mới cho bước tiếp theo
})
.then((baiViet) => {
console.log(`Người dùng này có ${baiViet.length} bài viết.`);
})
.catch((error) => {
console.error("Đã có lỗi trong chuỗi Promise:", error); // Bắt lỗi từ bất kỳ bước nào
});
💡 Mẹo nhớ nhanh:
.then()xử lý thành công,.catch()xử lý thất bại, còn.finally()luôn chạy để cleanup.
✨ Phần 3: async/await - Cách viết bất đồng bộ dễ đọc nhất
Dùng .then() và .catch() hoàn toàn ổn, nhưng khi flow dài hơn, code sẽ bắt đầu khó đọc. async/await là cú pháp hiện đại giúp code bất đồng bộ trông gần giống như code đồng bộ, tức là đọc từ trên xuống dưới rất tự nhiên.
🔹 1. async làm gì?
Khi đặt async trước một hàm, JavaScript hiểu rằng đây là hàm bất đồng bộ và hàm đó luôn trả về một Promise.
async function layTenNguoiDung() {
return "Tester"; // JavaScript sẽ tự bọc giá trị này thành Promise.resolve("Tester")
}
🔹 2. await làm gì?
await chỉ được dùng bên trong hàm async. Nó sẽ tạm dừng hàm đó cho đến khi Promise hoàn tất, rồi “mở gói” giá trị bên trong để bạn dùng tiếp.
function taiDuLieu() {
return new Promise((resolve) => {
setTimeout(() => resolve({ data: "Đây là dữ liệu người dùng" }), 2000);
});
}
async function xuLyDuLieu() {
console.log("Bắt đầu xử lý...");
try {
const response = await taiDuLieu(); // Chờ Promise hoàn tất rồi mới gán vào response
console.log("Tải thành công!", response.data);
} catch (error) {
console.error("Tải thất bại!", error); // Nếu Promise bị reject, sẽ nhảy vào catch
}
console.log("Dòng này chỉ chạy sau khi await kết thúc.");
}
xuLyDuLieu();
🔹 3. So sánh .then() với async/await
Cả hai đều dựa trên Promise. Khác biệt nằm ở cách viết. Với flow dài, async/await thường dễ đọc hơn nhiều.
// Cách dùng .then().catch()
layUserId()
.then((userId) => {
return layBaiVietCuaUser(userId);
})
.then((baiViet) => {
console.log(`Người dùng này có ${baiViet.length} bài viết.`);
})
.catch((error) => {
console.error("Đã có lỗi:", error);
});
// Cách dùng async/await
async function xuLyBaiViet() {
try {
console.log("Bắt đầu lấy ID người dùng...");
const userId = await layUserId(); // Chờ lấy xong userId
console.log("Đã có ID, bắt đầu lấy bài viết...");
const baiViet = await layBaiVietCuaUser(userId); // Chờ lấy xong bài viết
console.log(`Người dùng này có ${baiViet.length} bài viết.`);
} catch (error) {
console.error("Đã có lỗi:", error);
}
}
xuLyBaiViet();
🔹 4. Ứng dụng trong Playwright
Phần này đặc biệt quan trọng với automation. Hầu như mọi thao tác trong Playwright như page.goto(), fill(), click(), expect() đều trả về Promise. Vì vậy, bạn gần như luôn phải đi cùng await.
test("should login successfully", async ({ page }) => {
await page.goto("https://the-internet.herokuapp.com/login"); // Chờ trang tải xong
await page.locator("#username").fill("tomsmith"); // Chờ điền username xong
await page.locator("#password").fill("SuperSecretPassword!"); // Chờ điền password xong
await page.locator('button[type="submit"]').click(); // Chờ click xong
await expect(page.locator("#flash")).toContainText("You logged into a secure area!"); // Chờ assertion hoàn tất
});
⚠️ Lưu ý: Quên
awaittrong code automation là lỗi rất hay gặp. Nó có thể khiến test chạy sai thứ tự, flaky hoặc báo pass/fail không ổn định.
🧱 Phần 4: Modules với export và import
Khi dự án lớn dần, việc để toàn bộ code trong một file duy nhất sẽ nhanh chóng trở thành thảm họa: khó tìm, khó sửa và khó tái sử dụng. Modules giải quyết việc đó bằng cách cho phép bạn chia code thành nhiều file nhỏ, mỗi file phụ trách một nhóm chức năng rõ ràng.
🔹 1. Named export
Named export cho phép bạn export nhiều biến, hàm hoặc class từ cùng một file. Khi import, bạn phải dùng đúng tên đã export.
// File: utils.js
export const PI = 3.14; // Export một hằng số theo tên
export const cleanText = (text) => {
return text.trim().toLowerCase(); // Export một hàm tiện ích theo tên
};
export class TestUser {
constructor(username) {
this.username = username;
}
}
🔹 2. Default export
Mỗi file chỉ có thể có một export default. Kiểu này thường được dùng cho thứ quan trọng nhất, đại diện cho cả file.
// File: LoginPage.js
export default class LoginPage {
constructor(page) {
this.page = page;
this.usernameInput = page.locator("#username");
this.passwordInput = page.locator("#password");
this.submitButton = page.locator('button[type="submit"]');
}
async login(username, password) {
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
}
🔹 3. Import theo tên và import mặc định
Khi import named export, bạn dùng dấu ngoặc nhọn {}. Khi import default export, bạn không dùng ngoặc nhọn và có thể đặt tên bất kỳ.
// File: my-test.spec.js
import { cleanText, TestUser } from "./utils.js"; // Import theo tên
const cleaned = cleanText(" Hello World ");
const user = new TestUser("an.tester");
console.log(cleaned); // "hello world"
console.log(user.username); // "an.tester"
// File: my-test.spec.js
import LoginPage from "./LoginPage.js"; // Import mặc định
const loginPage = new LoginPage(page);
// Cũng có thể kết hợp cả hai nếu file có cả default export và named export
import LoginPage, { cleanText } from "./LoginPage.js";
💡 Mẹo nhớ nhanh: Một file có thể có nhiều named export, nhưng chỉ có đúng một default export.
🛡️ Phần 5: Xử lý lỗi với try...catch...finally
Code ngoài thực tế không bao giờ “lạc quan” mãi được. Element có thể không tồn tại, API có thể lỗi, dữ liệu trả về có thể hỏng. Nếu không có cơ chế xử lý lỗi, chỉ một lỗi nhỏ cũng có thể làm sập toàn bộ kịch bản test.
🔹 1. Cú pháp try...catch...finally
try: đặt phần code có khả năng gây lỗi vào đây.catch: chạy khi có lỗi xảy ra trong khối try.finally: luôn chạy, dù có lỗi hay không.
try {
console.log("Đang thực hiện tác vụ có thể lỗi...");
} catch (error) {
console.error("Đã xảy ra lỗi:", error.message); // Xử lý lỗi nếu có
} finally {
console.log("Luôn chạy để dọn dẹp hoặc log kết thúc.");
}
🔹 2. Xử lý lỗi khi không tìm thấy element
Đây là tình huống rất phổ biến trong automation UI. Nếu không bọc bằng try...catch, một lỗi tìm element có thể khiến toàn bộ script dừng ngay lập tức.
console.log("Bắt đầu kịch bản đăng nhập...");
try {
console.log("TRY: Đang cố gắng tìm và click vào nút đăng nhập...");
const loginButton = await page.locator("#login-button-that-does-not-exist");
await loginButton.click();
console.log("TRY: Đã click vào nút đăng nhập thành công!");
} catch (error) {
console.error("CATCH: Đã xảy ra lỗi! Không tìm thấy nút đăng nhập.");
console.error("Chi tiết lỗi:", error.message); // Ghi log để debug
await page.screenshot({ path: "Loi_Khong_Tim_Thay_Nut.png" }); // Chụp ảnh màn hình lỗi
} finally {
console.log("FINALLY: Hoàn thành khối xử lý đăng nhập.");
}
console.log("Tiếp tục thực hiện các bước test khác...");
🔹 3. Chủ động ném lỗi bằng throw new Error()
Không phải lúc nào lỗi cũng tự xuất hiện. Có những lúc bạn phải chủ động phát hiện điều kiện sai và ném lỗi để hệ thống xử lý theo luồng chuẩn.
async function getUserDataSafely(userId) {
try {
console.log("TRY: Gửi yêu cầu lấy thông tin người dùng...");
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error(`API trả về lỗi! Status: ${response.status}`); // Chủ động tạo lỗi nếu response không hợp lệ
}
const userData = await response.json();
console.log("Lấy dữ liệu thành công:", userData);
return userData;
} catch (error) {
console.error("CATCH: Không thể lấy dữ liệu người dùng.");
console.error("Lý do:", error.message);
return { name: "Người dùng không tồn tại", email: "N/A" }; // Trả về giá trị fallback để ứng dụng không sập
}
}
🔹 4. Vì sao try...catch quan trọng?
- Chống sập chương trình: một lỗi nhỏ không phá hỏng toàn bộ flow.
- Dễ debug hơn: bạn có chỗ để log lỗi, chụp screenshot, in response.
- Tách biệt logic rõ ràng: phần happy path ở
try, phần xử lý lỗi ởcatch. - Tạo test ổn định hơn: nhất là với hệ thống thật ngoài đời luôn có khả năng lỗi tạm thời.
🔹 Ghi nhớ nhanh cho Tester
- Promise là nền tảng của bất đồng bộ trong JavaScript.
.then(),.catch(),.finally()là cách xử lý Promise theo kiểu chaining.async/awaitgiúp code dễ đọc hơn nhưng vẫn chạy trên nền Promise.- Trong Playwright, gần như mọi thao tác quan trọng đều cần
await. - Modules giúp chia code thành nhiều file nhỏ để dễ quản lý và tái sử dụng.
- Named export dùng ngoặc nhọn khi import, còn default export thì không.
try...catchlà lưới an toàn bắt buộc nên có ở những chỗ dễ lỗi.throw new Error()rất hữu ích khi bạn muốn chủ động dừng flow sai và đẩy lỗi vàocatch.
✅ Kết luận: Bài 8 là bước chuyển từ JavaScript cơ bản sang JavaScript thực chiến hơn. Nếu nắm chắc Promise, async/await, modules và xử lý lỗi, bạn sẽ viết test automation sạch hơn, dễ bảo trì hơn và ổn định hơn rất nhiều.
