🎭 Playwright Event & Error Handling
Your Safety Net for Automated Testing
The Story: Meet Your Watchful Guardian
Imagine you have a smart security guard watching over your house. This guard doesn’t just stand there—they:
- Listen for sounds (events happening)
- Read notes left at the door (console messages)
- Spot broken windows (page errors)
- Know what to do when things go wrong (error handling)
- Stay calm when alarms randomly go off (flaky tests)
That’s exactly what Playwright’s event and error handling does for your automated tests!
🎧 Event Handling: The Listener
What is Event Handling?
Think of events like doorbells ringing. Every time something happens on a webpage—a file downloads, a popup appears, a new page opens—it’s like a doorbell ringing. Playwright can listen for these doorbells and react!
How Does It Work?
// Listen for file downloads
page.on('download', async (download) => {
console.log('File downloaded!');
await download.saveAs('my-file.pdf');
});
This is like telling your guard: “When someone delivers a package, save it in the storage room.”
Common Events You Can Listen For
graph LR A["🎭 Page Events"] --> B["📥 download"] A --> C["🪟 popup"] A --> D["📄 request"] A --> E["📨 response"] A --> F["💬 dialog"] A --> G["📝 console"] A --> H["❌ pageerror"]
Real Example: Handling Popups
// Before clicking, set up the listener
const popupPromise = page.waitForEvent('popup');
await page.click('button#open-new-window');
const popup = await popupPromise;
await popup.waitForLoadState();
console.log('Popup URL:', popup.url());
Think of it like this: You tell your guard, “A delivery truck is coming. Wait by the gate, and when it arrives, bring me the package.”
💬 Console Message Handling: Reading the Notes
What Are Console Messages?
Websites talk to developers by writing notes in the browser’s console. These notes can say:
- “Everything is fine!” ✅
- “Hmm, this might be a problem…” ⚠️
- “SOMETHING WENT WRONG!” ❌
Why Should We Listen?
Imagine your website is a restaurant kitchen. The chefs (JavaScript code) sometimes shout things:
- “Order ready!” (log)
- “We’re running low on salt!” (warning)
- “THE STOVE IS ON FIRE!” (error)
You want to hear ALL of these!
Capturing Console Messages
// Listen to ALL messages
page.on('console', msg => {
console.log(`[${msg.type()}]: ${msg.text()}`);
});
// Or filter by type
page.on('console', msg => {
if (msg.type() === 'error') {
console.log('ERROR found:', msg.text());
}
});
Checking for Problems in Tests
const errors = [];
page.on('console', msg => {
if (msg.type() === 'error') {
errors.push(msg.text());
}
});
await page.goto('https://example.com');
await page.click('#submit-form');
// After test, check for errors
expect(errors).toHaveLength(0);
The Analogy: Your guard keeps a notepad. Every time the kitchen shouts something bad, they write it down. At the end of the day, you check the notepad!
💥 Page Error Handling: Spotting the Broken Windows
What Are Page Errors?
Sometimes JavaScript on a webpage completely breaks. Not a small warning—a full crash! Like:
- Trying to use something that doesn’t exist
- Dividing by zero
- Running code that has typos
The pageerror Event
page.on('pageerror', error => {
console.log('Page crashed with:', error.message);
});
Real Example: Catching Page Errors
const pageErrors = [];
page.on('pageerror', error => {
pageErrors.push({
message: error.message,
stack: error.stack
});
});
await page.goto('https://buggy-website.com');
// Check if page had any crashes
if (pageErrors.length > 0) {
console.log('Found', pageErrors.length, 'errors!');
pageErrors.forEach(err => {
console.log(' -', err.message);
});
}
Difference: Console Error vs Page Error
graph LR A["Console Error"] --> B["Someone reported<br>a problem"] C["Page Error"] --> D["Something actually<br>broke/crashed"]
| Console Error | Page Error |
|---|---|
console.error("Oops") |
ReferenceError: x is not defined |
| Intentional message | Unintentional crash |
| Code continues running | Code might stop |
🛡️ Error Handling in Tests: Your Safety Net
Why Do Tests Need Error Handling?
Tests can fail for many reasons:
- Element not found
- Timeout waiting for something
- Network problems
- Unexpected popups
Without proper handling, one failure can ruin everything!
Try-Catch: Your Basic Safety Net
test('login test', async ({ page }) => {
try {
await page.goto('https://example.com/login');
await page.fill('#username', 'user@test.com');
await page.fill('#password', 'secret123');
await page.click('#login-btn');
await expect(page).toHaveURL('/dashboard');
} catch (error) {
console.log('Login failed:', error.message);
await page.screenshot({ path: 'login-failure.png' });
throw error; // Re-throw to fail the test
}
});
Soft Assertions: Don’t Stop at First Failure
Sometimes you want to check MANY things and see ALL failures, not just the first one.
test('check all fields', async ({ page }) => {
await page.goto('https://example.com/profile');
// Soft assertions continue even if one fails
await expect.soft(page.locator('#name'))
.toHaveValue('John');
await expect.soft(page.locator('#email'))
.toHaveValue('john@example.com');
await expect.soft(page.locator('#phone'))
.toHaveValue('123-456-7890');
// See ALL failures at the end
});
Think of it like this: A teacher checking a test with soft assertions marks ALL wrong answers, not just the first one!
Handling Timeouts Gracefully
test('wait for slow element', async ({ page }) => {
await page.goto('https://slow-website.com');
try {
// Wait up to 10 seconds
await page.waitForSelector('#slow-content', {
timeout: 10000
});
} catch (error) {
if (error.message.includes('Timeout')) {
console.log('Element took too long!');
// Maybe try an alternative action
await page.reload();
} else {
throw error; // Different error, re-throw
}
}
});
🎲 Flaky Test Handling: Taming the Unpredictable
What Are Flaky Tests?
A flaky test is like a light switch that sometimes works and sometimes doesn’t. Same test, same code, but:
- ✅ Monday: PASS
- ❌ Tuesday: FAIL
- ✅ Wednesday: PASS
- ❌ Thursday: FAIL
Why Do Tests Become Flaky?
graph TD A["🎲 Flaky Test Causes"] --> B["⏱️ Timing Issues"] A --> C["🌐 Network Delays"] A --> D["🎯 Animation/Transitions"] A --> E["📊 Test Data Changes"] A --> F["🖥️ Resource Limits"]
Solution 1: Automatic Retries
// In playwright.config.js
export default {
retries: 2, // Retry failed tests twice
// Different retries for CI vs local
retries: process.env.CI ? 2 : 0,
};
Like this: If the light switch doesn’t work, try flipping it again!
Solution 2: Better Waiting Strategies
❌ Bad: Hard-coded waits
// DON'T DO THIS!
await page.waitForTimeout(5000);
✅ Good: Wait for specific conditions
// Wait until element is visible
await page.waitForSelector('#result', {
state: 'visible'
});
// Wait until network is idle
await page.waitForLoadState('networkidle');
// Wait until specific text appears
await expect(page.locator('#status'))
.toHaveText('Complete');
Solution 3: test.describe.configure for Retries
test.describe('checkout flow', () => {
// This specific group retries 3 times
test.describe.configure({ retries: 3 });
test('complete purchase', async ({ page }) => {
// This test will retry up to 3 times
});
});
Solution 4: Mark Known Flaky Tests
// This test is known to be flaky
test('sometimes fails', async ({ page }) => {
test.fixme(); // Skip but track it
});
// Or skip entirely
test.skip('broken test', async ({ page }) => {
// Won't run
});
Solution 5: Trace on First Retry
// In playwright.config.js
export default {
use: {
trace: 'on-first-retry',
},
};
This records a detailed trace ONLY when a test fails and is being retried. Perfect for debugging flaky tests!
🎯 Putting It All Together
Here’s a complete example using ALL the concepts:
import { test, expect } from '@playwright/test';
test('robust shopping test', async ({ page }) => {
// Collect all issues
const consoleErrors = [];
const pageErrors = [];
// Set up listeners FIRST
page.on('console', msg => {
if (msg.type() === 'error') {
consoleErrors.push(msg.text());
}
});
page.on('pageerror', error => {
pageErrors.push(error.message);
});
// Handle potential popup
page.on('dialog', async dialog => {
await dialog.accept();
});
try {
// Navigate and interact
await page.goto('https://shop.example.com');
await page.click('#add-to-cart');
// Wait properly (not with timeout!)
await expect(page.locator('#cart-count'))
.toHaveText('1');
await page.click('#checkout');
// Soft assertions for form validation
await expect.soft(page.locator('#total'))
.toBeVisible();
await expect.soft(page.locator('#pay-btn'))
.toBeEnabled();
} catch (error) {
// Take screenshot on failure
await page.screenshot({
path: `failure-${Date.now()}.png`
});
throw error;
}
// Final checks
expect(consoleErrors).toHaveLength(0);
expect(pageErrors).toHaveLength(0);
});
🎉 Summary: Your Guardian’s Toolkit
| Tool | What It Does | When to Use |
|---|---|---|
page.on('event') |
Listens for events | Downloads, popups, dialogs |
page.on('console') |
Catches console messages | Debug info, warnings |
page.on('pageerror') |
Catches JS crashes | Find broken code |
try-catch |
Handles test failures | Take screenshots, cleanup |
expect.soft() |
Collects all failures | Check multiple things |
retries |
Runs test again | Flaky tests |
waitForSelector |
Smart waiting | Replace hardcoded waits |
🚀 You’re Ready!
You now have a complete toolkit for handling anything that goes wrong in your Playwright tests. Remember:
- Set up listeners early - Before navigating to pages
- Wait smartly - Never use hardcoded timeouts
- Catch errors gracefully - Take screenshots, log details
- Use retries wisely - For genuinely flaky tests, not broken code
- Collect evidence - Console logs, errors, traces
Your tests are now protected by the best security guard in town! 🎭✨
