Retries and Recovery
Retries and Recovery
Stepwright provides multiple levels of retry and recovery to handle flaky tests and transient failures.
Step-Level Retries
Basic Retry Configuration
Configure retries for individual steps:
.step('Click flaky button', async (ctx) => { await ctx.page.click('#sometimes-hidden-button');}, { retry: { times: 3, // Retry up to 3 times delay: 1000, // Wait 1 second between retries },})Backoff Strategies
Choose how delay increases between retries:
.step('Linear backoff', async (ctx) => { await ctx.page.click('button');}, { retry: { times: 3, delay: 1000, backoff: 'linear', // 1s, 2s, 3s },}).step('Exponential backoff', async (ctx) => { await ctx.page.click('button');}, { retry: { times: 3, delay: 1000, backoff: 'exponential', // 1s, 2s, 4s },})When to Use Step Retries
Step retries are ideal for:
- Flaky element interactions - Buttons that occasionally miss clicks
- Network timing issues - Waiting for slow responses
- Race conditions - Element appears then disappears
// Good use case: flaky button.step('Submit payment', async (ctx) => { await ctx.page.click('#pay-button');}, { retry: { times: 2, delay: 500 },})
// Bad use case: stateful operations.step('Create user', async (ctx) => { await api.createUser(); // Might create duplicate users on retry!}, { retry: { times: 2 }, // Dangerous!})Checkpoint-Level Retries
Basic Checkpoint Retry
Retry an entire group of steps:
.checkpoint('Login', { maxRetries: 3 }) .step('Enter email', async (ctx) => { await ctx.page.fill('#email', ctx.data.email); }) .step('Enter password', async (ctx) => { await ctx.page.fill('#password', ctx.data.password); }) .step('Submit', async (ctx) => { await ctx.page.click('button[type="submit"]'); await ctx.page.waitForURL('**/dashboard'); }).endCheckpoint()If any step in the checkpoint fails, all steps restart from the beginning.
Setup and Teardown
Run code before retries and after completion:
.checkpoint('Checkout', { maxRetries: 2,
// Runs before each attempt (including retries) setup: async () => { console.log('Starting checkout attempt'); },
// Runs after checkpoint (success or final failure) teardown: async () => { console.log('Checkout completed'); },})When to Use Checkpoint Retries
Checkpoint retries are ideal for:
- Multi-step flows that must complete together
- State-dependent sequences where partial completion is invalid
- Environment flakiness where the whole page might be unstable
// Good: Payment flow must complete atomically.checkpoint('Payment', { maxRetries: 2 }) .step('Enter card', ...) .step('Enter CVV', ...) .step('Submit payment', ...).endCheckpoint()
// If CVV entry fails, we restart from entering the card// This ensures we don't submit partial payment infoRequired vs Optional Checkpoints
Required Checkpoints
By default, checkpoints are required. If they fail, the script fails:
.checkpoint('Login', { required: true }) // Default .step('Authenticate', ...).endCheckpoint()
// Script stops if login failsOptional Checkpoints
Optional checkpoints don’t fail the script:
.checkpoint('Analytics', { required: false }) .step('Send tracking event', async (ctx) => { // If this fails, script continues await sendAnalytics(ctx.data); }).endCheckpoint()
.checkpoint('Core Flow', { required: true }) .step('Main business logic', ...).endCheckpoint()Error Handling
Step-Level Error Handling
Handle errors within a step:
.step('Risky operation', async (ctx) => { try { await ctx.page.click('#optional-element'); } catch (error) { ctx.log('Optional element not found, continuing...'); }}, { // Also fires on unhandled errors onError: async (error, ctx) => { ctx.log('Step failed:', error.message); await ctx.screenshot('error-state'); },})Global Configuration
Set default behavior for all steps:
.config({ stopOnFailure: true, // Stop script on first failure screenshotOnFailure: true, // Capture screenshot on failure domOnFailure: true, // Capture DOM on failure})Recovery Strategies
Skip Failing Steps
Mark steps as non-critical:
.step('Optional enhancement', async (ctx) => { await ctx.page.click('#premium-feature');}, { critical: false, // Script continues if this fails})Conditional Execution
Skip steps based on previous results:
.step('Retry alternative', async (ctx) => { // Check if previous step failed const lastResult = ctx.results[ctx.results.length - 1]; if (lastResult?.status === 'failed') { await ctx.page.click('#alternative-button'); }})State Snapshots
Save and restore state for recovery:
.step('Save state before risky operation', async (ctx) => { ctx.snapshot();})
.step('Risky operation', async (ctx) => { // If this fails, we can restore await ctx.page.click('#dangerous-button');})
.step('Recover if needed', async (ctx) => { const lastResult = ctx.results[ctx.results.length - 1]; if (lastResult?.status === 'failed') { ctx.restoreLastSnapshot(); await ctx.page.goto(ctx.data.fallbackUrl); }})Timeout Configuration
Step Timeout
.step('Long operation', async (ctx) => { await ctx.page.waitForResponse('**/slow-api');}, { timeout: 60000, // 60 seconds})Default Timeout
.config({ defaultTimeout: 30000, // 30 seconds for all steps})Result Inspection
Check retry attempts in results:
const result = await script.run();
for (const step of result.steps) { if (step.retryAttempts > 0) { console.log(`${step.name} required ${step.retryAttempts} retries`); }}Best Practices
Start with Checkpoint Retries
Prefer checkpoint retries over step retries for related operations:
// Better: Atomic retry of related steps.checkpoint('Form Submission', { maxRetries: 2 }) .step('Fill form', ...) .step('Submit', ...).endCheckpoint()
// Worse: Individual retries leave inconsistent state.step('Fill form', ..., { retry: { times: 2 } }).step('Submit', ..., { retry: { times: 2 } })Use Reasonable Limits
// Good: Limited retries with delays.checkpoint('Login', { maxRetries: 3 })
// Bad: Too many retries, will take forever.checkpoint('Login', { maxRetries: 10 })Log Retry Attempts
.step('Retry with logging', async (ctx) => { ctx.log('Attempting action...'); await ctx.page.click('button');}, { retry: { times: 3, delay: 1000, }, beforeRun: (ctx) => { ctx.log('Starting attempt'); }, onError: (error, ctx) => { ctx.log('Attempt failed:', error.message); },})Next Steps
- Artifacts - Capture screenshots and DOM on failure
- Reporters - Output failure information
- Debugging Guide - Debug failing scripts