microsoft/playwright · error · Error
Function "${name}" has been already registered
Error message
Function "${name}" has been already registered What it means
`exposeBinding`/`exposeFunction` on a BrowserContext checks `_pageBindings` for the name; a duplicate name at context level is rejected because the global binding would collide.
Source
Thrown at packages/playwright-core/src/server/browserContext.ts:379
async exposePlaywrightBindingIfNeeded() {
this._playwrightBindingExposed ??= (async () => {
await this.doExposePlaywrightBinding();
this.bindingsInitScript = PageBinding.createInitScript(this);
this.initScripts.push(this.bindingsInitScript);
await this.doAddInitScript(this.bindingsInitScript);
await this.safeNonStallingEvaluateInAllFrames(this.bindingsInitScript.source, 'main');
})();
return await this._playwrightBindingExposed;
}
needsPlaywrightBinding(): boolean {
return this._playwrightBindingExposed !== undefined;
}
async exposeBinding(progress: Progress, name: string, playwrightBinding: frames.FunctionWithSource, forClient?: unknown, noGlobal?: boolean): Promise<PageBinding> {
if (this._pageBindings.has(name))
throw new Error(`Function "${name}" has been already registered`);
for (const page of this.pages()) {
if (page.getBinding(name))
throw new Error(`Function "${name}" has been already registered in one of the pages`);
}
await progress.race(this.exposePlaywrightBindingIfNeeded());
const binding = new PageBinding(this, name, playwrightBinding, noGlobal);
binding.forClient = forClient;
this._pageBindings.set(name, binding);
try {
await progress.race(this.doAddInitScript(binding.initScript));
await progress.race(this.safeNonStallingEvaluateInAllFrames(binding.initScript.source, 'main'));
return binding;
} catch (error) {
this._pageBindings.delete(name);
throw error;
}
}
View on GitHub (pinned to c8fc3bf8d3)
Solutions
- Use the Disposable returned by exposeBinding and dispose before re-exposing the same name
- Use a unique name per binding (namespace by test/module)
- Expose at page level if you need per-page isolation
- Track registrations in a Set to avoid duplicates
Example fix
// before
await context.exposeBinding('cb', h);
await context.exposeBinding('cb', h); // throws
// after
const d1 = await context.exposeBinding('cb', h);
await d1.dispose();
await context.exposeBinding('cb', h); // ok Defensive patterns
Strategy: validation
Validate before calling
const registered = new Set<string>();
async function exposeOnce(ctx: BrowserContext, name: string, fn: Function) {
if (registered.has(name)) return;
registered.add(name);
const d = await ctx.exposeBinding(name, fn as any);
d[Symbol.dispose] && (d as any)[Symbol.dispose]();
}
// Prefer: dispose before re-registering the same name. Prevention
- Dispose the Disposable returned by exposeBinding before re-exposing a name
- Namespace binding names per test/module to avoid collisions
- Expose at context level once in setup, not per test
When it happens
Trigger: `context.exposeBinding('foo', ...)` called twice with the same name; or re-exposing a name already registered via `context.exposeFunction` without disposing first.
Common situations: Test fixtures that expose a binding per test without cleanup; helper modules registering the same binding name across suites; refactoring page-level bindings to context-level.
Related errors
- Function "${name}" has been already registered in one of the
- Please use browser.newContext()
- page: expected Page or Frame
- Page has been closed.
- "deviceScaleFactor" option is not supported with null "viewp
AI-assisted analysis of microsoft/playwright@c8fc3bf8d3 (2026-08-12).
Data as JSON: /api/errors/7193a9f4a922be56.
Report an issue: GitHub.