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

  1. Drop the page-level exposeFunction call and rely on the context-level binding instead.
  2. If a distinct page binding is genuinely needed, rename it so it does not collide with the context binding.
  3. 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

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


AI-assisted analysis of microsoft/playwright@c8fc3bf8d3 (2026-08-12). Data as JSON: /api/errors/ddd0c4a8070b21e2. Report an issue: GitHub.