microsoft/playwright · error · Error

Function "${name}" has been already registered in one of the

Error message

Function "${name}" has been already registered in one of the pages

What it means

Same exposeBinding flow, second guard: even if the context doesn't have the name, if any already-created page has a page-level binding with the same name, Playwright rejects it to prevent shadowing.

Source

Thrown at packages/playwright-core/src/server/browserContext.ts:382

      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;
    }
  }

  async removeExposedBinding(binding: PageBinding) {
    if (this._pageBindings.get(binding.name) !== binding)
      return;

View on GitHub (pinned to c8fc3bf8d3)

Solutions

  1. Use distinct names for page vs context bindings
  2. Expose at context level before creating pages, or stick to one level
  3. Dispose page-level bindings before promoting to context

Example fix

// before
await page.exposeBinding('cb', h);
await context.exposeBinding('cb', h); // throws

// after
await page.exposeBinding('pageCb', h);
await context.exposeBinding('ctxCb', h);
Defensive patterns

Strategy: validation

Validate before calling

const ctxNames = new Set<string>();
const pageNames = new Set<string>();
async function exposeCtx(ctx: BrowserContext, name: string, fn: Function) {
  if (ctxNames.has(name) || pageNames.has(name))
    throw new Error(`Binding '${name}' already registered (page or context).`);
  ctxNames.add(name);
  await ctx.exposeBinding(name, fn as any);
}

Prevention

When it happens

Trigger: `page.exposeBinding('foo', ...)` followed by `context.exposeBinding('foo', ...)`; or any ordering where pages already exist with a colliding binding.

Common situations: Mixing page-level and context-level `exposeBinding`/`exposeFunction` with the same name; adding context bindings after pages were created; global setup registering context bindings after per-page setup ran.

Related errors


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