microsoft/playwright · error · Error
Function "${name}" has been already registered in the browse
Error message
Function "${name}" has been already registered in the browser context What it means
Thrown by Page.exposeBinding when no page-level duplicate exists but a BrowserContext-level binding with the same name was already registered. Playwright treats context bindings as inherited by every page, so adding a page binding with a colliding name would shadow the context one ambiguously and is rejected.
Source
Thrown at packages/playwright-core/src/server/page.ts:357
}
opener(): Page | undefined {
return this._opener;
}
mainFrame(): frames.Frame {
return this.frameManager.mainFrame();
}
frames(): frames.Frame[] {
return this.frameManager.frames();
}
async exposeBinding(progress: Progress, name: string, playwrightBinding: frames.FunctionWithSource, noGlobal?: boolean): Promise<PageBinding> {
if (this._pageBindings.has(name))
throw new Error(`Function "${name}" has been already registered`);
if (this.browserContext._pageBindings.has(name))
throw new Error(`Function "${name}" has been already registered in the browser context`);
await progress.race(this.browserContext.exposePlaywrightBindingIfNeeded());
const binding = new PageBinding(this, name, playwrightBinding, noGlobal);
this._pageBindings.set(name, binding);
try {
await progress.race(this.delegate.addInitScript(binding.initScript));
await progress.race(this.safeNonStallingEvaluateInAllFrames(binding.initScript.source, 'main'));
return binding;
} catch (error) {
this._pageBindings.delete(name);
throw error;
}
}
async removeExposedBinding(binding: PageBinding) {
if (this._pageBindings.get(binding.name) !== binding)
return;
this._pageBindings.delete(binding.name);
await this.delegate.removeInitScripts([binding.initScript]);View on GitHub (pinned to c8fc3bf8d3)
Solutions
- Drop the page-level exposeFunction call and rely on the context-level binding instead.
- If a distinct page binding is genuinely needed, rename it so it does not collide with the context binding.
- Centralize all binding registration in one place (preferably context scope) and remove duplicates elsewhere.
Example fix
// before
await context.exposeFunction('getToken', () => token);
await page.exposeFunction('getToken', () => other); // throws
// after
await context.exposeFunction('getToken', () => token); // pages inherit it Defensive patterns
Strategy: validation
Validate before calling
// Decide scope up front and never mix names across scopes
async function exposeScoped(context, page, name, fn, scope) {
if (scope === 'context')
return context.exposeFunction(name, fn); // pages inherit, no per-page call needed
// page scope: ensure no context binding collides by tracking context names yourself
return page.exposeFunction(name, fn);
} Prevention
- Centralize binding registration in one module to avoid context/page name collisions.
- When promoting a binding from page to context scope, delete the old page-level call.
- Namespace binding names by feature to make collisions obvious.
When it happens
Trigger: Calling browserContext.exposeFunction('foo', fn) and then page.exposeFunction('foo', fn) on a page in that context; mixing context-wide and page-wide bindings that share a name; a shared helper that adds page bindings on top of an app that already exposes the same name at context scope.
Common situations: A test framework fixture exposes a helper at context scope while a legacy per-page helper still calls page.exposeFunction with the same name; upgrading code from per-page to per-context exposure without removing the old call.
Related errors
- Function "${name}" has been already registered
- Function "${name}" is not exposed
- Please use browser.newContext()
- page: expected Page or Frame
- Function "${name}" has been already registered
AI-assisted analysis of microsoft/playwright@c8fc3bf8d3 (2026-08-12).
Data as JSON: /api/errors/ddd0c4a8070b21e2.
Report an issue: GitHub.