Skip to content

[Bug]: Uncaught "Assertion error" when a Chromium page crashes while trace screenshots or video are recorded #42949

Description

Version

1.63.0

Steps to reproduce

  1. npm i playwright@1.63.0 && npx playwright install chromium
  2. Save this as repro.mjs:
import { chromium } from 'playwright';

// A page that repaints on every frame, so screencast frames keep arriving.
const html = '<p id=n></p><script>let i = 0; (function tick() { n.textContent = i++; requestAnimationFrame(tick); })();</script>';

const browser = await chromium.launch();
console.log(`Chromium ${browser.version()}`);
for (let i = 1; i <= 50; i++) {
  const context = await browser.newContext();
  await context.tracing.start({ screenshots: true }); // or: browser.newContext({ recordVideo: { dir } })
  const pages = await Promise.all(Array.from({ length: 8 }, () => context.newPage()));
  await Promise.all(pages.map(page => page.setContent(html)));
  await pages[0].waitForTimeout(100);
  await Promise.all(pages.map(async page => {
    const crashed = new Promise(resolve => page.once('crash', resolve));
    page.goto('chrome://crash').catch(() => {});
    await crashed;
  }));
  await context.tracing.stop();
  await context.close();
  console.log(`iteration ${i}: no assertion`);
}
await browser.close();
console.log('Not reproduced');
  1. node repro.mjs

The Node process dies within the first four iterations: 10 of 10 runs (5 on Node 22.17.0, 5 on Node 23.10.0).

The same thing in Playwright Test, with one page per test:

// playwright.config.mjs
import { defineConfig } from '@playwright/test';

export default defineConfig({
  use: { trace: 'retain-on-failure' },
});
// crash.spec.mjs
import { test } from '@playwright/test';

test('a page that crashes while it is being recorded', async ({ page }) => {
  // A page that repaints on every frame, so screencast frames keep arriving.
  await page.setContent('<p id=n></p><script>let i = 0; (function tick() { n.textContent = i++; requestAnimationFrame(tick); })();</script>');
  await page.waitForTimeout(100);
  const crashed = page.waitForEvent('crash');
  page.goto('chrome://crash').catch(() => {});
  await crashed;
});

npx playwright test --repeat-each=40 --workers=1: in two runs, 6 and 9 of 40 failed with trace: 'retain-on-failure', and 2 and 3 of 40 with use: { video: 'on' } instead (Node 22.17.0).

Expected behavior

The page emits crash and the script or test carries on, as it does when nothing is being recorded (0 assertion errors in 240 crashes, see the table below).

Actual behavior

An uncaught exception from Playwright's CDP client ends the Node process:

node_modules/playwright-core/lib/coreBundle.js:838
    throw new Error(message || "Assertion error");
          ^

Error: Assertion error
    at assert (node_modules/playwright-core/lib/coreBundle.js:838:11)
    at _CRSession._onMessage (node_modules/playwright-core/lib/coreBundle.js:35014:11)
    at CRConnection._onMessage (node_modules/playwright-core/lib/coreBundle.js:34951:20)
    at Immediate.<anonymous> (node_modules/playwright-core/lib/coreBundle.js:39453:30)
    at process.processImmediate (node:internal/timers:485:21)
    at process.callbackTrampoline (node:internal/async_hooks:130:17)

Node.js v22.17.0

In Playwright Test the test fails with nothing but this, with no stack and no location, so it looks like an ordinary flaky test:

  1) crash.spec.mjs:3:1 › a page that crashes while it is being recorded

    Error: Assertion error

Additional context

Cause. From DEBUG=pw:protocol (session ids shortened):

SEND ► {"id":432,"method":"Page.screencastFrameAck","params":{"sessionId":1},"sessionId":"D13E…"}
◀ RECV {"method":"Inspector.targetCrashed","params":{},"sessionId":"D13E…"}
◀ RECV {"id":432,"result":{},"sessionId":"D13E…"}
  1. While trace screenshots or video are recorded, every screencast frame is acknowledged with Page.screencastFrameAck (crPage.ts#L952-L962).
  2. The renderer crashes while an ack is in flight. Inspector.targetCrashed leads to CRSession._markAsCrashed() (crPage.ts#L908-L911, crConnection.ts#L127-L130), which rejects and clears every pending callback (crConnection.ts#L194-L202).
  3. Chromium still answers the ack, after the crash event. In CRSession._onMessage (crConnection.ts#L154-L174) the id is no longer known, and the reply carries no error, so it is not the ignored -32001 case either. It falls through to assert(!object.id, …), which throws from the transport's setImmediate callback, where no user code can catch it.

When it happens. Measured with a variant of the script above, 240 renderer crashes per row (30 contexts × 8 pages), Node 23.10.0:

Recording Page Crash Assertion errors
nothing repainting Page.crash 0
nothing static Page.crash 0
trace, snapshots only repainting Page.crash 0
trace, screenshots static Page.crash 0
trace, screenshots repainting Page.crash 19
trace, screenshots repainting chrome://crash 31
trace, screenshots + snapshots repainting Page.crash 12, 26, 29, 39 (four runs)
recordVideo repainting Page.crash 28
recordVideo repainting chrome://crash 49

It takes a screencast with frames flowing; how the renderer dies does not matter. Note that trace: 'retain-on-failure' records screenshots during every test, so every test in which a page crashes is exposed, even though the trace is thrown away when the test passes.

A possible fix. Ignore a reply that arrives after the session was marked as crashed, the same way as the -32001 case:

    } else if (object.id && (this._crashed || object.error?.code === -32001)) {
      // A reply to a command whose callback was already rejected because the
      // page crashed, or a message to a closed session: ignore it.
    } else {

With this change applied to a local copy of playwright-core 1.63.0, the script above ran all 50 iterations (400 crashes) in 3 of 3 runs; with the change reverted in the same copy, 2 of 2 runs died within five iterations. The screencast ack is simply the command that is almost always in flight: any reply that arrives after Inspector.targetCrashed takes the same path.

How we ran into it. A test that crashes the renderer on purpose, to check that data already written to IndexedDB survives, failed about one run in eight with only Error: Assertion error, and about one in three under CPU load. It ran with trace: 'retain-on-failure'; PWDEBUGIMPL=1 revealed the stack. We now kill the browser process from outside instead, which closes the connection with nothing in flight.

The code involved is unchanged on main (b9a34ac).

Environment

System:
  OS: macOS 26.7
  CPU: (11) arm64 Apple M3 Pro
  Memory: 18.00 GB
Binaries:
  Node: 22.17.0 (also reproduced on 23.10.0)
npmPackages:
  playwright: 1.63.0
  @playwright/test: 1.63.0
Browsers:
  Chromium 153.0.8010.12 (bundled with 1.63.0), headless

Activity

  1. dgozman commented on Sep 28, 2026

    @dgozman
    Collaborator

    This will be fixed by #42936.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions