microsoft/playwright · error · Error

Function "${name}" has been already registered

Error message

Function "${name}" has been already registered

What it means

Thrown by Page.exposeBinding when a page-level binding with the same name has already been registered on this Page. Playwright forbids two bindings sharing a name because each one is installed as a global function on the page, so a duplicate would silently overwrite the earlier callback. The check happens before any init script is injected, so the second call fails fast.

Source

Thrown at packages/playwright-core/src/server/page.ts:355

    const fileChooser = new FileChooser(handle, multiple);
    this.emit(Page.Events.FileChooser, fileChooser);
  }

  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;

View on GitHub (pinned to c8fc3bf8d3)

Solutions

  1. Give each binding a unique name, or dispose the previous binding (the Disposable returned by exposeFunction/exposeBinding) before re-registering.
  2. Move page.exposeFunction to a once-per-page location (e.g. before page.goto, or in a fixture that runs once when the page is created).
  3. Expose the function at the BrowserContext level (browserContext.exposeFunction) once, so every page in the context inherits it without per-page re-registration.

Example fix

// before
for (const t of tests) {
  await page.exposeFunction('getValue', () => 1); // throws on 2nd iteration
}
// after
await context.exposeFunction('getValue', () => 1); // once per context
// or dispose the returned handle
const handle = await page.exposeFunction('getValue', () => 1);
await handle[Symbol.asyncDispose](); // before re-registering
Defensive patterns

Strategy: validation

Validate before calling

// Before re-exposing, check the page has no such binding by tracking your own set
const exposed = new Set<string>();
async function exposeOnce(page, name, fn) {
  if (exposed.has(name))
    throw new Error(`'${name}' already exposed; dispose it first or use a unique name`);
  const handle = await page.exposeFunction(name, fn);
  exposed.add(name);
  handle[Symbol.asyncDispose] && await handle[Symbol.asyncDispose]().then(() => exposed.delete(name));
  return handle;
}

Prevention

When it happens

Trigger: Calling page.exposeFunction('foo', fn) or page.exposeBinding('foo', fn) twice on the same Page object with the identical name; calling page.exposeBinding then page.exposeFunction with the same name; re-exposing inside a beforeEach hook without scoping the binding to the page lifetime.

Common situations: Test suites that re-run setup per test without creating a fresh page; helpers that wrap exposeFunction and are invoked more than once; refactoring that moved exposeFunction into a loop or a reused fixture.

Related errors


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