| name | error-handling |
| description | Error handling patterns for Playwright tests including network failures, timeouts, and retries. Use when testing app error states, implementing retry logic, handling flaky elements, configuring timeouts, or adding graceful degradation tests.
|
Error Handling Skill
Patterns and strategies for handling errors gracefully in Playwright tests — network failures, timeouts, flaky elements, and retry logic.
Core Principles
- Expect failures - Tests should handle expected error scenarios gracefully
- Fail fast with clarity - Provide meaningful error messages when tests fail
- Retry intelligently - Use retries for genuine flakiness, not to mask real bugs
- Isolate failure points - One assertion per concern, clear error boundaries
- Log contextually - Capture useful diagnostics on failure
Table of Contents
Network Error Handling
Testing Offline / Network Failure Scenarios
import { test, expect } from '@playwright/test';
test.describe('Network Error Handling', () => {
test('should display error message when API is unreachable', async ({ page }) => {
await page.route('**/api/products', (route) => {
route.abort('connectionrefused');
});
await page.goto('/products');
await expect(page.getByRole('alert')).toHaveText('Unable to load products. Please try again.');
await expect(page.getByRole('button', { name: 'Retry' })).toBeVisible();
});
test('should handle server errors gracefully', async ({ page }) => {
await page.route('**/api/orders', (route) => {
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Internal Server Error' }),
});
});
await page.goto('/orders');
await expect(page.getByText('Something went wrong')).toBeVisible();
await expect(page.getByRole('button', { name: 'Retry' })).toBeVisible();
});
test('should handle 404 responses', async ({ page }) => {
await page.route('**/api/products/999', (route) => {
route.fulfill({
status: 404,
contentType: 'application/json',
body: JSON.stringify({ error: 'Product not found' }),
});
});
await page.goto('/products/999');
await expect(page.getByText('Product not found')).toBeVisible();
await expect(page.getByRole('link', { name: 'Back to Products' })).toBeVisible();
});
test('should handle timeout on slow API responses', async ({ page }) => {
await page.route('**/api/search', async (route) => {
await new Promise((resolve) => setTimeout(resolve, 30000));
route.fulfill({ status: 200, body: '[]' });
});
await page.goto('/search?q=laptop');
await expect(page.getByText('Request timed out')).toBeVisible({ timeout: 15000 });
});
});
Intercepting and Validating Error Responses
test('should log failed API requests for debugging', async ({ page }) => {
const failedRequests: string[] = [];
page.on('requestfailed', (request) => {
failedRequests.push(`${request.method()} ${request.url()} - ${request.failure()?.errorText}`);
});
let requestCount = 0;
await page.route('**/api/data', (route) => {
requestCount++;
if (requestCount === 1) {
route.abort('connectionreset');
} else {
route.fulfill({ status: 200, body: JSON.stringify({ data: 'success' }) });
}
});
await page.goto('/dashboard');
await expect(page.getByText('Dashboard loaded')).toBeVisible();
expect(failedRequests.length).toBe(1);
expect(failedRequests[0]).toContain('connectionreset');
});
Timeout Strategies
Configuring Timeouts Properly
import { defineConfig } from '@playwright/test';
export default defineConfig({
timeout: 60000,
expect: {
timeout: 10000,
},
use: {
navigationTimeout: 30000,
actionTimeout: 15000,
},
});
Per-Test Timeout Overrides
test('should complete large file upload', async ({ page }) => {
test.setTimeout(120000);
await page.goto('/upload');
await page.getByLabel('File').setInputFiles('large-file.zip');
await page.getByRole('button', { name: 'Upload' }).click();
await expect(page.getByText('Upload complete')).toBeVisible({ timeout: 90000 });
});
Assertion-Level Timeouts
test('should load dashboard widgets', async ({ page }) => {
await page.goto('/dashboard');
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible({ timeout: 5000 });
await expect(page.getByTestId('analytics-widget')).toBeVisible({ timeout: 15000 });
await expect(page.getByTestId('revenue-chart')).toBeVisible({ timeout: 20000 });
});
Retry Patterns
Playwright Built-in Retries
import { defineConfig } from '@playwright/test';
export default defineConfig({
retries: process.env.CI ? 2 : 0,
reporter: [
['html', { open: 'never' }],
['list'],
],
});
Custom Retry Logic for Specific Operations
export async function retryOperation<T>(
operation: () => Promise<T>,
options: {
maxRetries?: number;
baseDelay?: number;
maxDelay?: number;
onRetry?: (attempt: number, error: Error) => void;
} = {}
): Promise<T> {
const { maxRetries = 3, baseDelay = 1000, maxDelay = 10000, onRetry } = options;
let lastError: Error;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error as Error;
if (attempt === maxRetries) break;
const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
onRetry?.(attempt, lastError);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw lastError!;
}
Using Retry Helper in Tests
import { test, expect } from '@playwright/test';
import { retryOperation } from '../utils/retry-helpers';
test('should create order via API with retry', async ({ page }) => {
const order = await retryOperation(
async () => {
const response = await page.request.post('/api/orders', {
data: { productId: '123', quantity: 1 },
});
expect(response.ok()).toBeTruthy();
return response.json();
},
{
maxRetries: 3,
baseDelay: 2000,
onRetry: (attempt, error) => {
console.log(`Order creation attempt ${attempt} failed: ${error.message}`);
},
}
);
expect(order.id).toBeDefined();
});
Retry vs Fix - Decision Guide
Graceful Degradation
Testing App Behavior When Features Fail
test.describe('Graceful Degradation', () => {
test('should show cached data when API fails', async ({ page }) => {
await page.goto('/products');
await expect(page.getByRole('heading', { name: 'Products' })).toBeVisible();
await page.route('**/api/products', (route) => route.abort('connectionrefused'));
await page.reload();
await expect(page.getByText('Showing cached data')).toBeVisible();
await expect(page.getByRole('list')).not.toBeEmpty();
});
test('should disable dependent features when service is down', async ({ page }) => {
await page.route('**/api/recommendations', (route) => {
route.fulfill({ status: 503 });
});
await page.goto('/products/123');
await expect(page.getByRole('heading', { name: 'Product Details' })).toBeVisible();
await expect(page.getByText('Recommendations unavailable')).toBeVisible();
});
test('should handle partial API response', async ({ page }) => {
await page.route('**/api/user/profile', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
name: 'John Doe',
email: 'john@example.com',
}),
});
});
await page.goto('/profile');
await expect(page.getByText('John Doe')).toBeVisible();
await expect(page.getByTestId('avatar')).toHaveAttribute('src', /default-avatar/);
});
});
Custom Error Messages
Providing Context in Assertions
test('should display correct product pricing', async ({ page }) => {
await page.goto('/products/wireless-headphones');
const price = page.getByTestId('product-price');
await expect(price, 'Product price should show discounted amount after coupon').toHaveText(
'$71.99'
);
await expect(
page.getByRole('alert'),
'Discount success notification should appear after applying coupon'
).toBeVisible();
});
test('bad example', async ({ page }) => {
await page.goto('/products/wireless-headphones');
await expect(page.getByTestId('product-price')).toHaveText('$71.99');
});
Custom Error Helper
export function assertionMessage(context: {
component: string;
expected: string;
scenario?: string;
}): string {
const base = `${context.component} should ${context.expected}`;
return context.scenario ? `${base} when ${context.scenario}` : base;
}
await expect(
cartPage.cartTotal,
assertionMessage({
component: 'Cart total',
expected: 'reflect discounted price',
scenario: 'SAVE10 coupon is applied',
})
).toHaveText('$71.99');
Try-Catch Patterns
When to Use Try-Catch in Tests
test.describe('Order Management', () => {
let orderId: string;
test.beforeEach(async ({ page }) => {
try {
const response = await page.request.post('/api/orders', {
data: { productId: '123', quantity: 1 },
});
const order = await response.json();
orderId = order.id;
} catch (error) {
console.error('Failed to create test order:', error);
throw new Error(`Test setup failed: Could not create order. ${error}`);
}
});
test.afterEach(async ({ page }) => {
try {
if (orderId) {
await page.request.delete(`/api/orders/${orderId}`);
}
} catch (error) {
console.warn(`Cleanup warning: Could not delete order ${orderId}:`, error);
}
});
test('should display order details', async ({ page }) => {
await page.goto(`/orders/${orderId}`);
await expect(page.getByRole('heading', { name: 'Order Details' })).toBeVisible();
await expect(page.getByTestId('order-id')).toHaveText(orderId);
});
});
test('bad example', async ({ page }) => {
try {
await page.goto('/products');
await expect(page.getByText('Products')).toBeVisible();
} catch {
console.log('Test failed but continuing...');
}
});
Error Boundary Testing
Testing React/Vue/Angular Error Boundaries
test.describe('Error Boundary Testing', () => {
test('should show error boundary when component crashes', async ({ page }) => {
await page.goto('/products?simulateError=true');
await expect(page.getByText('Something went wrong')).toBeVisible();
await expect(page.getByRole('button', { name: 'Try Again' })).toBeVisible();
await expect(page.getByRole('navigation')).toBeVisible();
});
test('should recover after error boundary retry', async ({ page }) => {
await page.goto('/products?simulateError=once');
await expect(page.getByText('Something went wrong')).toBeVisible();
await page.getByRole('button', { name: 'Try Again' }).click();
await expect(page.getByRole('heading', { name: 'Products' })).toBeVisible();
});
});
Handling Flaky Elements
Waiting for Stable State
test('should handle element that appears after animation', async ({ page }) => {
await page.goto('/products');
await page.getByRole('button', { name: 'Add to Cart' }).click();
const notification = page.getByRole('alert');
await expect(notification).toBeVisible();
await expect(notification).toHaveText('Item added to cart');
await notification.waitFor({ state: 'hidden' });
});
test('bad: hard-coded wait for animation', async ({ page }) => {
await page.goto('/products');
await page.getByRole('button', { name: 'Add to Cart' }).click();
await page.waitForTimeout(2000);
});
Handling Modals and Overlays
test('should handle loading overlay before interacting', async ({ page }) => {
await page.goto('/dashboard');
await page.getByTestId('loading-overlay').waitFor({ state: 'hidden' });
await page.getByRole('button', { name: 'Export Data' }).click();
});
Screenshot on Failure
Automatic Screenshot Capture
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
screenshot: 'only-on-failure',
trace: 'on-first-retry',
video: 'on-first-retry',
},
});
Custom Screenshot Helper
import { Page } from '@playwright/test';
export async function captureDebugInfo(page: Page, testName: string): Promise<void> {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const prefix = `debug-${testName}-${timestamp}`;
await page.screenshot({
path: `test-results/screenshots/${prefix}.png`,
fullPage: true,
});
const logs: string[] = [];
page.on('console', (msg) => logs.push(`[${msg.type()}] ${msg.text()}`));
const networkErrors: string[] = [];
page.on('requestfailed', (req) => {
networkErrors.push(`${req.method()} ${req.url()} - ${req.failure()?.errorText}`);
});
}
Common Anti-Patterns
What NOT to Do
test('bad: swallowed error', async ({ page }) => {
try {
await page.goto('/products');
await expect(page.getByText('Products')).toBeVisible();
} catch {
}
});
test('bad: massive timeout', async ({ page }) => {
test.setTimeout(300000);
});
test('bad: useless try-catch', async ({ page }) => {
try {
await page.goto('/checkout');
} catch (error) {
throw error;
}
});
test('bad: timeout as error simulation', async ({ page }) => {
await page.goto('/products');
await page.waitForTimeout(60000);
});
What TO Do Instead
test('good: clear assertion', async ({ page }) => {
await page.goto('/products');
await expect(
page.getByText('Products'),
'Products heading should be visible after page load'
).toBeVisible();
});
test('good: scoped timeouts', async ({ page }) => {
test.setTimeout(60000);
await expect(page.getByTestId('chart')).toBeVisible({ timeout: 15000 });
});
test('good: contextual error', async ({ page }) => {
const response = await page.request.get('/api/products');
if (!response.ok()) {
throw new Error(
`Failed to fetch products: ${response.status()} ${response.statusText()}`
);
}
});
test('good: simulated error', async ({ page }) => {
await page.route('**/api/products', (route) => route.abort('connectionrefused'));
await page.goto('/products');
await expect(page.getByRole('alert')).toHaveText('Unable to load products');
});
Error Handling Checklist
Related Resources