Skip to content

using declaration inside page.evaluate() throws "Cannot read properties of undefined (reading 'd')" #41511

Description

@tinovyatkin

Summary

A using declaration (Explicit Resource Management) inside a page.evaluate() callback throws TypeError: Cannot read properties of undefined (reading 'd') at runtime in the page, even though the target browser supports using natively.

Playwright's bundled Babel transform lowers using into a call to a helper function (_usingCtx) that it emits at module scope — a sibling of the test function, not inside it. But page.evaluate(fn) ships the callback to the browser via fn.toString(), which captures only the function body, not its module-scope siblings. So in the page, _usingCtx is undefined and the lowered code crashes.

Repro

// using.spec.ts
import { test, expect } from '@playwright/test';

test('using inside page.evaluate', async ({ page }) => {
  await page.goto('about:blank');
  const disposed = await page.evaluate(() => {
    let disposed = false;
    {
      using r = { [Symbol.dispose]: () => { disposed = true; } };
      void r;
    }
    return disposed;
  });
  expect(disposed).toBe(true);
});
playwright.config.ts: { testDir: '.', projects: [{ name: 'chromium' }] }

Actual

Error: page.evaluate: TypeError: Cannot read properties of undefined (reading 'd')
    at eval (eval at evaluate (:303:30), <anonymous>:14:17)

Expected

The callback runs and returns true (the block-scoped resource is disposed at block exit).

Root cause

Playwright's transform (@babel/plugin-transform-explicit-resource-management, bundled in lib/transform/babelBundle.js) lowers the callback to:

function _usingCtx2() { /* … emitted at MODULE scope … */ }

// inside the test:
page.evaluate(() => {
  try {
    var _usingCtx = _usingCtx2();   // <-- _usingCtx2 is undefined after fn.toString()
    const r = _usingCtx.u({ [Symbol.dispose]() {} });
    ...

page.evaluate serializes the callback with fn.toString(), which does not carry the module-scope _usingCtx2 helper into the page → _usingCtx2 is undefined_usingCtx.u/.d throws "reading 'd'".

This is the same class of issue the code already guards against for class fields — babelTransformOptions sets assumptions: { setPublicClassFields: true } with the comment "Without this, babel defines a top level function that breaks playwright evaluates." The explicit-resource-management lowering reintroduces exactly that top-level-helper pattern.

Evidence it's the toString() boundary, not browser support

The bundled Chromium supports using natively — passing the same code as a string (which Babel never transforms) works:

// passes ✓
const disposed = await page.evaluate(`(() => {
  let disposed = false;
  { using r = { [Symbol.dispose]: () => { disposed = true; } }; void r; }
  return disposed;
})()`);

So the gap is specifically: a function-literal callback using using is lowered to depend on an out-of-band helper that evaluate's serialization drops.

The lowering is also unnecessary for modern targets

The transform runs unconditionally. babelTransformOptions pushes the bare @babel/plugin-transform-explicit-resource-management plugin and passes no targets (and sets browserslistConfigFile: false, configFile: false), so it lowers using regardless of whether the target browser supports it natively — which the bundled Chrome for Testing 149 does.

Notably, Playwright bundles @babel/core 7.28.3, and Babel 7.28.0 added target-aware Explicit Resource Management to @babel/preset-env (babel#17355, via babel-compat-data) — i.e. the machinery to skip the ERM transform when targets already support using is present in the very Babel that Playwright ships. Because Playwright applies the bare transform plugin (not preset-env) with no targets, that target-awareness never kicks in.

A couple of fixes are therefore possible independently:

  1. Make the ERM lowering self-contained for the evaluate-serialization path (inline the helper into the transformed function), mirroring the existing setPublicClassFields assumption — this fixes the toString() boundary generally.
  2. Feed Playwright's transform a targets (and/or honor the project tsconfig target) so the transform is skipped entirely when the test browser supports the feature natively — no helper, no lowering.

Environment

  • @playwright/test: 1.61.1
  • Bundled browser: Chrome for Testing 149.0.7827.55 (playwright chromium v1228)
  • Node: v22.22.2
  • OS: Linux

Notes / possible directions

  • The same would affect any feature Babel lowers into emitted out-of-function helpers inside an evaluate body (e.g. some down-leveled decorators).
  • Workaround for users today: write the evaluate body as a string, or replace using with the equivalent try/finally (which is what it desugars to and needs no helper).

Metadata

Metadata

Assignees

Labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions