microsoft/playwright · error · Error
Function "${name}" is not exposed
Error message
Function "${name}" is not exposed What it means
Thrown by PageBinding.dispatch when the page sends a binding invocation for a name that no PageBinding is registered for. dispatch is the server-side receiver for calls to window.__playwright__binding__; if the binding was disposed, removed, or never installed under that name, Playwright cannot route the call and reports it as not exposed.
Source
Thrown at packages/playwright-core/src/server/page.ts:1084
readonly initScript: InitScript;
readonly cleanupScript: string;
forClient?: unknown;
constructor(parent: BrowserContext | Page, name: string, playwrightFunction: frames.FunctionWithSource, noGlobal?: boolean) {
super(parent);
this.name = name;
this.playwrightFunction = playwrightFunction;
this.initScript = new InitScript(parent, `globalThis['${kBindingsControllerProperty}'].addBinding(${JSON.stringify(name)}, ${!!noGlobal})`);
this.cleanupScript = `globalThis['${kBindingsControllerProperty}'].removeBinding(${JSON.stringify(name)})`;
}
static async dispatch(page: Page, payload: string, context: dom.FrameExecutionContext) {
const { name, seq, serializedArgs } = JSON.parse(payload) as BindingPayload;
try {
assert(context.world);
const binding = page.getBinding(name);
if (!binding)
throw new Error(`Function "${name}" is not exposed`);
if (!Array.isArray(serializedArgs))
throw new Error(`serializedArgs is not an array. This can happen when Array.prototype.toJSON is defined incorrectly`);
const args = serializedArgs.map(a => parseEvaluationResultValue(a));
const result = await binding.playwrightFunction({ frame: context.frame, page, context: page.browserContext }, ...args);
context.evaluateExpressionHandle(`arg => globalThis['${kBindingsControllerProperty}'].deliverBindingResult(arg)`, { isFunction: true }, { name, seq, result }).catch(e => debugLogger.log('error', e));
} catch (error) {
context.evaluateExpressionHandle(`arg => globalThis['${kBindingsControllerProperty}'].deliverBindingResult(arg)`, { isFunction: true }, { name, seq, error }).catch(e => debugLogger.log('error', e));
}
}
override async dispose(): Promise<void> {
await this.parent.removeExposedBinding(this);
}
}
export class InitScript extends DisposableObject {
readonly source: string;
View on GitHub (pinned to c8fc3bf8d3)
Solutions
- Keep the binding registered for as long as the page might call it; do not dispose the handle until the page is closed or navigated to a state that no longer uses it.
- Re-expose the function (page.exposeFunction) after navigation/disposal if the page still needs it.
- Avoid calling the internal bindings controller directly from page code; use the exposed function reference returned by exposeFunction.
Example fix
// before
const handle = await page.exposeFunction('cb', fn);
await handle[Symbol.asyncDispose]();
await page.evaluate(() => window.cb()); // dispatch finds no binding
// after
await page.evaluate(() => window.cb());
await handle[Symbol.asyncDispose](); // dispose only after the page is done Defensive patterns
Strategy: validation
Validate before calling
// Ensure the binding is (still) registered before the page can call it
const exposed = new Set<string>();
const handle = await page.exposeFunction('cb', fn); exposed.add('cb');
// before disposing in teardown:
if (exposed.has('cb')) { await handle[Symbol.asyncDispose](); exposed.delete('cb'); } Prevention
- Keep bindings registered for as long as the page might invoke them.
- Do not dispose exposeFunction handles until the page is closed or navigated away from code that uses them.
- Avoid invoking the internal bindings controller directly from page code.
When it happens
Trigger: Page-side code invokes an exposed function after the binding was disposed (e.g. the test disposed the handle but the page still holds a reference); a binding removed via cleanupScript races with an in-flight call; page code manually triggers the binding controller with an arbitrary name.
Common situations: Disposing a page.exposeFunction handle while the page still calls it asynchronously; navigating away and back so init scripts are re-evaluated but the server-side binding map was cleared; manual/low-level use of the bindings controller.
Related errors
- Function "${name}" has been already registered
- Function "${name}" has been already registered in the browse
- The object has been collected to prevent unbounded heap grow
- Cannot find object to "${method}": ${guid}
- Unknown new child: ${params.guid}
AI-assisted analysis of microsoft/playwright@c8fc3bf8d3 (2026-08-12).
Data as JSON: /api/errors/9821ca612957e4dd.
Report an issue: GitHub.