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

  1. Use the Disposable returned by exposeBinding and dispose before re-exposing the same name
  2. Use a unique name per binding (namespace by test/module)
  3. Expose at page level if you need per-page isolation
  4. 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

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


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