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:
- 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.
- 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).
Summary
A
usingdeclaration (Explicit Resource Management) inside apage.evaluate()callback throwsTypeError: Cannot read properties of undefined (reading 'd')at runtime in the page, even though the target browser supportsusingnatively.Playwright's bundled Babel transform lowers
usinginto a call to a helper function (_usingCtx) that it emits at module scope — a sibling of the test function, not inside it. Butpage.evaluate(fn)ships the callback to the browser viafn.toString(), which captures only the function body, not its module-scope siblings. So in the page,_usingCtxisundefinedand the lowered code crashes.Repro
Actual
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 inlib/transform/babelBundle.js) lowers the callback to:page.evaluateserializes the callback withfn.toString(), which does not carry the module-scope_usingCtx2helper into the page →_usingCtx2 is undefined→_usingCtx.u/.dthrows "reading 'd'".This is the same class of issue the code already guards against for class fields —
babelTransformOptionssetsassumptions: { 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 supportThe bundled Chromium supports
usingnatively — passing the same code as a string (which Babel never transforms) works:So the gap is specifically: a function-literal callback using
usingis lowered to depend on an out-of-band helper thatevaluate's serialization drops.The lowering is also unnecessary for modern targets
The transform runs unconditionally.
babelTransformOptionspushes the bare@babel/plugin-transform-explicit-resource-managementplugin and passes notargets(and setsbrowserslistConfigFile: false,configFile: false), so it lowersusingregardless of whether the target browser supports it natively — which the bundled Chrome for Testing 149 does.Notably, Playwright bundles
@babel/core7.28.3, and Babel 7.28.0 added target-aware Explicit Resource Management to@babel/preset-env(babel#17355, viababel-compat-data) — i.e. the machinery to skip the ERM transform whentargetsalready supportusingis present in the very Babel that Playwright ships. Because Playwright applies the bare transform plugin (not preset-env) with notargets, that target-awareness never kicks in.A couple of fixes are therefore possible independently:
evaluate-serialization path (inline the helper into the transformed function), mirroring the existingsetPublicClassFieldsassumption — this fixes thetoString()boundary generally.targets(and/or honor the projecttsconfigtarget) so the transform is skipped entirely when the test browser supports the feature natively — no helper, no lowering.Environment
@playwright/test: 1.61.1Notes / possible directions
evaluatebody (e.g. some down-leveled decorators).evaluatebody as a string, or replaceusingwith the equivalenttry/finally(which is what it desugars to and needs no helper).