Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

page.on(eventName, handler) attaches a persistent listener to a Playwright Page. Use it when a test must observe events such as requests, responses, console messages, dialogs, popups, downloads, or page errors. Use page.once() or page.waitForEvent() for one occurrence, and remove long-lived listeners with the same function reference via page.removeListener().

This guide shows the correct timing, payloads, cleanup, network lifecycle, dialog safety, popup and download patterns, troubleshooting, and when event observation should be replaced with request routing.

What page.on() does

A Playwright Page represents a browser tab (and, in Chromium extensions, a background page). It emits named events. The JavaScript API follows Node’s EventEmitter style:

page.on('eventName', handler);

The handler remains active until the page is closed or you remove it. The event name determines both when the callback runs and which object it receives. The complete, release-specific list is in the Playwright Page API reference.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Persistent, one-time, and removable listeners

function logRequest(request) {
  console.log('A request was made:', request.url());
}

page.on('request', logRequest);

// Later, remove exactly this function reference.
page.removeListener('request', logRequest);

// For a single future occurrence:
page.once('load', () => console.log('The next load completed'));

An inline arrow function cannot be removed later unless you saved it in a variable. Prefer named functions for listeners that live longer than one action.

Choose the event that matches the signal

Need Event Payload What it tells you
Page lifecycle load, domcontentloaded, close Usually the page or no argument Document milestones or tab closure
Browser console output console ConsoleMessage A page called a console method
Uncaught page exception pageerror Error JavaScript escaped the page without being caught
JavaScript dialog dialog Dialog alert, confirm, or prompt
New tab or window popup Page A page opened by this page
File download download Download A download was created
Outgoing network request request Request The page issued a request
Response headers and status response Response A server response arrived
Body finished downloading requestfinished Request A request completed successfully
Transport failure requestfailed Request DNS, connection, TLS, or other network failure

Events are observational. A Request supplied to a request listener is read-only; it does not let you change headers or the response.

Network event handlers

Log requests and responses

page.on('request', request => {
  console.log('→', request.method(), request.url());
});

page.on('response', response => {
  console.log('←', response.status(), response.url());
});

page.on('requestfailed', request => {
  console.error('✕', request.url(), request.failure()?.errorText);
});

For a successful request, Playwright reports request, then response when status and headers arrive, then requestfinished after the body downloads. A transport failure produces requestfailed instead of successful completion. An HTTP 404 or 503 is still a response; inspect response.status() rather than expecting requestfailed.

Observe versus intercept

Use listeners to log, measure, or assert what happened. To modify, fulfill, or abort a request, use page.route() or browserContext.route():

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
await page.route('**/api/**', async route => {
  const request = route.request();
  if (request.method() === 'GET') {
    await route.continue();
  } else {
    await route.abort();
  }
});

Once routing matches a request, your handler must explicitly continue, fulfill, or abort it. Routing changes behavior; page.on('request') does not.

Dialogs: always resolve them

A dialog listener must call accept() or dismiss(). An unresolved dialog can block clicks and navigation.

page.on('dialog', async dialog => {
  console.log(dialog.type(), dialog.message());
  if (dialog.type() === 'prompt') {
    await dialog.accept('value supplied by the test');
  } else {
    await dialog.accept();
  }
});

If neither the page nor its browser context has a dialog listener, Playwright automatically dismisses dialogs. Register a listener only when the test needs to inspect or control the dialog.

Popups and downloads: wait before the action

For a one-off event caused by a click, create the promise first. Waiting after the click can miss a fast event.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Popup

const popupPromise = page.waitForEvent('popup');
await page.getByText('Open popup').click();
const popup = await popupPromise;
await popup.waitForLoadState();
console.log('Popup URL:', popup.url());

A popup becomes available when it has navigated to its initial URL and begun receiving a response. If you must observe or route that initial request, attach listeners or routing at the browser-context level.

Download

const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: 'Export' }).click();
const download = await downloadPromise;
await download.saveAs('artifacts/export.csv');

Use a test-specific path and check download.failure() when diagnosing a missing file.

Console messages and page errors

console and pageerror answer different debugging questions:

page.on('console', message => {
  console.log(`[browser:${message.type()}]`, message.text());
});

page.on('pageerror', error => {
  console.error('Uncaught page exception:', error.message);
});

A console message is an intentional call such as console.log or console.error. A page error is an uncaught exception. Do not treat ordinary console output as proof that the page threw an exception.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Lifecycle and frame-related listeners

Lifecycle events help diagnose navigation timing:

page.on('domcontentloaded', () => console.log('DOM parsed'));
page.on('load', () => console.log('Subresources loaded'));
page.on('close', () => console.log('Tab closed'));

page.on('frameattached', frame => console.log('Attached:', frame.url()));
page.on('framedetached', frame => console.log('Detached:', frame.url()));
page.on('framenavigated', frame => console.log('Navigated:', frame.url()));

Choose the milestone your assertion needs. load does not mean that every later API call or lazy resource has completed.

A maintainable listener pattern

  1. Register early. Attach persistent diagnostics immediately after creating the page, before navigation.
  2. Use narrow handlers. Filter by URL, method, message type, or frame instead of logging every event in a large suite.
  3. Keep handlers non-blocking. If asynchronous work is required, await it deliberately and avoid unbounded queues.
  4. Clean up. Remove temporary listeners after the assertion, especially when reusing a page in a worker.
  5. Fail intentionally. Throwing inside an event callback can produce confusing asynchronous failures; capture the relevant data and make the test assertion in the main flow when practical.
function onApiResponse(response) {
  if (response.url().includes('/api/orders')) {
    console.log('Orders status:', response.status());
  }
}

page.on('response', onApiResponse);
try {
  await page.getByRole('button', { name: 'Refresh orders' }).click();
} finally {
  page.removeListener('response', onApiResponse);
}

Common problems and fixes

The event never fires

  • Confirm the exact event name and register before navigation or the triggering action.
  • For a popup or download, use waitForEvent() before clicking.
  • Check that the action really targets this page; a popup may be owned by another page in the context.

The test hangs after an alert

Your dialog handler probably never resolved the dialog. Call accept() or dismiss() on every branch, or remove the listener to restore Playwright’s automatic dismissal.

A 404 appears as a network failure

It should not. Log response.status() for HTTP errors. Reserve requestfailed for transport-level failures.

Changing a request in page.on('request') does nothing

Listeners cannot mutate requests. Move the logic to page.route() or browserContext.route(), and ensure every routed request is continued, fulfilled, or aborted.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Duplicate log lines appear

The same listener may have been registered repeatedly, or a page is reused across tests. Keep one function reference, remove it in cleanup, and avoid registering inside loops unless that is intentional.

A newer event is unavailable

Event availability depends on the installed Playwright release. The API reference marks consoleMessages as added in v1.56 and dialogclosed in v1.63; verify your project’s version before using them.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Performance, reliability, and test design

Event callbacks run while Playwright processes browser activity. Logging every request and response can create substantial output and slow a suite, so filter URLs and disable verbose listeners outside diagnosis. Prefer a one-off waitForEvent() for a single expected signal; reserve persistent page.on() listeners for cross-step diagnostics or assertions.

Do not use event timing as a substitute for a condition the user actually needs. For UI readiness, a locator assertion or explicit application signal is usually more stable than assuming that load means all asynchronous work is finished. Set realistic timeouts on event waits and include the URL or action in timeout diagnostics.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Or skip the browser setup

If your goal is simply a clean website image rather than browser-event testing, ScreenshotNeo provides a single screenshot request. It accepts cookie and consent banners as a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each cleanup step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server gives Claude, Cursor, and other MCP clients take_screenshot, get_page_info, and capture_pdf tools.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

See the ScreenshotNeo documentation for all options, including full-page and element captures, device and retina settings, PDF output, custom scripts, request blocking, cookies, headers, geolocation, caching, signed links, asynchronous jobs, bulk capture, and the usage API. The free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

Frequently Asked Questions

Can I register the same Playwright event more than once?

Yes. Each registration runs independently, so repeated setup can produce duplicate callbacks. Store and remove handlers deliberately when a page is reused.

Should I use page.on(‘response’) to assert an API returned 200?

You can inspect response.status(), but make the assertion in the main test flow or use a targeted wait so the failure is tied clearly to the action that triggered the API call.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

What is the difference between page.once() and page.waitForEvent()?

once() installs a callback for the next event. waitForEvent() returns a promise that you can await and coordinate with the action producing the event.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.