Key points
- Collect both console errors and uncaught page errors, because a listener for one misses the other.
- Attach the listeners before the page loads, so errors thrown during loading are collected too.
- Allow a known message only by its exact text, with a written reason for each entry.
How do you catch browser console errors in tests?
To catch browser console errors, a test listens for console messages and uncaught exceptions from the moment the page opens. It collects each error and fails if any error is not on a short allowlist. In Playwright, these arrive as the page's console and pageerror events.
A console error is a message in the browser's developer console that reports a failure. Page code writes one with console.error(), which MDN's console reference describes as output "with the error log level." The browser writes its own, e.g. when an image fails to load.
An uncaught exception is an error that no try and catch block handles. The browser stops the code that threw it and fires the window's error event. Unless a handler cancels it, the browser also prints the error in the console.
Either kind often marks a failed step, e.g. a request whose data never arrived, while the rest of the page looks right. Unlike a crash in a command-line application, which ends with a nonzero exit code, neither kind fails a Playwright test by default. Collected errors work as an implicit test oracle, a failure signal that needs no expected value.
What do you need before you start?
You need these parts:
- A browser test framework. The steps use Playwright's test runner,
@playwright/test, with TypeScript. Another browser test framework has its own hooks and defaults, e.g. Cypress fails a test on an uncaught exception by default but not on a console error. - Tests that open pages. The listeners collect errors only from pages that an end-to-end test opens.
- A headless browser in CI. The suite runs in a headless browser on the continuous integration (CI) server, where nobody reads the console.
- The same build locally and in CI. Test the same kind of build in both places, because React logs some warnings with
console.erroronly in development builds, e.g. a missingkeyprop.
How do you catch browser console errors step by step?
Here is an illustrative example. A developer at Acme Co. asks a coding agent to "Let customers edit their delivery address during checkout." The agent's Playwright test fills in the address, checks that it appears, and passes.
1. Add a fixture that listens for errors
Save this test fixture as tests/fixtures.ts. With { auto: true }, it runs before each test body, so the listeners exist before the test opens a URL. After the test, it fails on each error not on the allowlist.
import { test as base, expect } from "@playwright/test";
import { allowed } from "./allowed-errors";
export const test = base.extend<{ browserErrors: string[] }>({
browserErrors: [async ({ page }, use) => {
const errors: string[] = [];
page.on("console", (msg) => {
if (!["error", "assert"].includes(msg.type())) return;
const load = msg.text().startsWith("Failed to load resource");
errors.push(load ? `${msg.text()} at ${msg.location().url}` : msg.text());
});
page.on("pageerror", (error) => errors.push(error.message));
await use(errors);
expect(errors.filter(e => !allowed.includes(e)), "browser errors").toEqual([]);
}, { auto: true }],
});
Playwright's Page class documents both events. In Chromium, the browser's own error messages also arrive as console errors, e.g. Failed to load resource: the server responded with a status of 404 (Not Found). That text leaves out which request failed, so the fixture adds the URL from msg.location() to those messages only.
2. Start an empty allowlist
Save the allowlist as tests/allowed-errors.ts. It starts empty, so each error fails a test.
// Each entry is one exact message. Add a comment with its reason and owner.
export const allowed: string[] = [];
3. Import the fixture in each test
The checkout test changes only its import:
import { expect } from "@playwright/test";
import { test } from "./fixtures";
test("customer can change the delivery address", async ({ page }) => {
await page.goto("https://shop.example.com/checkout");
await page.getByLabel("Delivery address").fill("12 Elm Street");
await page.getByRole("button", { name: "Save address" }).click();
await expect(page.getByText("12 Elm Street")).toBeVisible();
});
4. Run the suite and read the failure
Run npx playwright test. The address check passes, but the fixture fails the test. This is the output, shortened:
1) tests/checkout.spec.ts:4:5 › customer can change the delivery address
Error: browser errors
- Array []
+ Array [
+ "Cannot read properties of undefined (reading 'items')",
+ ]
at fixtures.ts:13
The message comes from a TypeError in the save button's click handler. The handler reads cart.items before the cart data loads, which leaves the cart summary empty. The address saved, but the cart emptied.
5. Fix the bug and run the check in CI
The agent loads the cart data first, and the test passes. Acme runs the suite on each pull request in GitHub Actions, where a failed test fails the job.
This example is simplified. A real suite would also save a trace of each failed test.
Which console messages should you allow?
Add an entry only when the team agrees that a message needs no fix in this change, and copy its full text from the test output. These cases are common:
- Errors a test causes on purpose. A test that makes the API return a 500 status also gets a
Failed to load resourceerror in Chromium. Allow it in that test only. The test takesbrowserErrorsas a fixture argument, likepage, and removes that message before it ends. - Third-party scripts. A chat script from another host can log errors that the team cannot fix. Serve an empty stub in its place with
page.route, or allow its exact message. - Known bugs with a ticket. The bug's message stays on the list, with the ticket ID in a comment, until the fix merges.
- Warnings. The fixture in step 1 skips
warningmessages. Fail only on warnings the team has listed as bugs.
A hydration error belongs on no list, because it means the server's HTML and the browser's render differ.
What changes when a coding agent writes the code?
A coding agent usually reports "done" once the build and the unit tests pass. Neither runs the checkout page in a browser, so the TypeError from step 4 never reaches the terminal.
An agent's browser tool can report console errors too, e.g. Playwright MCP returns them when the agent calls browser_console_messages. This kind of browser automation checks a page only when the agent opens it, and a fixture checks it on each run.
When the check fails, an agent can hide the error instead of fixing the app. It can add the message to the allowlist, or change cart.items to cart?.items ?? [] so nothing throws and the cart summary stays empty. Each edit makes the run pass and keeps the bug.
Have a person review each change to the fixture and the allowlist, e.g. through a CODEOWNERS file. Send the agent a bug report with the error text and the test's actions as reproduction steps.
What are common mistakes?
These mistakes let errors through while the suite passes:
- Attaching listeners after the page loads. A listener receives only the events that fire after it is attached. As a fallback,
page.consoleMessages()andpage.pageErrors(), added in Playwright 1.56, return up to 200 recent entries. Since 1.59, they return only entries after the last navigation by default. - Ending the test before late errors arrive. The fixture reads the list when the test body returns, so it misses errors that timers or slow responses throw later. End each test with a check that waits for the last update.
- Listening for console messages only. Uncaught exceptions and unhandled promise rejections arrive only through
pageerror. - Checking only the error type. A failed
console.assert()call arrives with the typeassert, noterror, in Chromium, so the fixture in step 1 collects both types. Playwright's ConsoleMessage docs list each type. - Watching one page only. The fixture in step 1 listens on one page, so it misses errors that a second tab logs, e.g. a payment page. The browser context's
consoleandweberrorevents cover each of its pages. - Allowing messages by fragment. A pattern that matches
Failed to load resourcealso allows the app's own failed API calls. Match the full message, with its URL.
How do you check that it worked?
Plant errors that the fixture must catch, and mark those tests as expected failures with test.fail():
test("the check catches a thrown error", async ({ page }) => {
test.fail();
await page.setContent(`<script>throw new Error("planted")</script>`);
});
Add a second planted test that calls console.error(). Both tests fail as expected, so the run passes. If the fixture misses a planted error, the runner reports "Expected to fail, but passed." Then check these points:
- A page with no errors passes.
- Removing one allowlist entry makes the test that needs it fail with that exact message.
- A CI run on a branch with a planted error and no
test.fail()fails the job.
A smoke test that opens each main page with the fixture extends the check. A clean run shows only that those pages logged no errors, not that every journey works.
FAQs
What is the difference between a console error and a page error?
A console error is a message written to the browser console at the error level, by page code or by the browser itself. A page error is an uncaught exception or an unhandled promise rejection. Playwright reports the first through its console event and the second through pageerror.
Should a test fail on console warnings?
A console warning usually should not fail a test, because libraries and deprecations produce many that a team cannot fix at once. Fail only on warnings the team has listed as bugs.
How do you capture console messages in Playwright?
Console messages in Playwright arrive through the page's console event, which passes each message with its type and text. Uncaught exceptions arrive through the pageerror event instead. Since Playwright 1.56, the page's consoleMessages and pageErrors methods also return recent entries.
What is an uncaught exception in a browser?
An uncaught exception in a browser is an error that no try and catch block handles, e.g. a TypeError thrown in a click handler. The browser stops that handler and prints the error in the console. Playwright still reports it as a page error, not as a console message.