Skip to content

[Bug]: page.evaluate with exposeFunctions and world: "utility" fails with an internal TypeError #42686

Closed

Description

Filing this as an issue as asked in #42649.

Version

1.64.0-next. Originally reproduced at aeebee1d2; packages/playwright-core/src/client/jsHandle.ts has not been touched since that commit, so it still applies.

Steps to reproduce

exposeFunctions and the world option added in #42635 cannot be combined, but passing both crashes inside the injected code rather than saying so.

await page.evaluate(async ({ cb }) => await cb(17), { cb: x => x * 2 },
    { exposeFunctions: true, world: 'utility' });
// page.evaluate: TypeError: Cannot read properties of undefined (reading 'callBinding')

Same error from frame.evaluate and locator.evaluate. Reproduced on chromium, firefox and webkit.

Why it happens

PageBinding.createInitScript installs the bindings controller as an init script, and init scripts only run in the main world, so globalThis[kBindingsControllerProperty] is undefined in the utility world. Runtime.addBinding is registered without a world, so the transport is there but the controller that dispatches through it is not.

That also makes a documented claim false. docs/src/api/params.md says an exposed function is "technically accessible from all frames and worlds of the page", in both js-evaluate-expose-functions and js-init-script-expose-functions:

await page.exposeFunction('f', x => x * 2);
await page.evaluate(() => window.f?.(21) ?? 'NOT PRESENT');                                   // 42
await page.evaluate(() => window.f?.(21) ?? 'NOT PRESENT', undefined, { world: 'utility' });  // 'NOT PRESENT'

Possible directions

Either make bindings reachable from the utility world, or reject the combination with a clear error the way JSHandleDispatcher.evaluateExpression already does for a world it cannot serve. Either way the two doc sentences want correcting, since they promise worlds today.

I had a patch for the second option in #42649 with the guard in assertEvaluateOptions, which every evaluate entry point already calls, plus the doc correction and one test. Happy to reopen that, rework it toward the first option, or leave it entirely, whichever you prefer. I have no view on which layer you want this solved at.

I am a freshman in college trying to make myself useful on real projects, so if I have misread the intent here just say and I will drop it.

Activity

  1. github-actions commented on Sep 14, 2026

    @github-actions
    Contributor

    Hi, I'm the Playwright bot and I took a first look at this.

    Reproduced on tip-of-tree (1.64.0-next, sha 52a0e72), on macOS, in chromium, firefox and webkit. Your analysis holds on both counts: { exposeFunctions: true, world: 'utility' } fails inside the injected code with TypeError: Cannot read properties of undefined (reading 'callBinding') (firefox words it as can't access property "callBinding", globalThis[kBindingsControllerProperty] is undefined), and an already-exposed function is genuinely not visible from the utility world — the second page.evaluate returns 'NOT PRESENT', not 42, so the "accessible from all frames and worlds of the page" sentence in docs/src/api/params.md (lines 591 and 597) is wrong as written.

    Which of the two directions to take — reach bindings into the utility world, or reject the combination — is a maintainer call, so I'll leave that one alone.

    Minimal repro
    test('exposeFunctions cannot be combined with world: utility', async ({ page }) => {
      // page.evaluate: TypeError: Cannot read properties of undefined (reading 'callBinding')
      const result = await page.evaluate(async ({ cb }) => await cb(17), { cb: (x: number) => x * 2 },
          { exposeFunctions: true, world: 'utility' });
      expect(result).toBe(34);
    });
    
    test('exposeFunction is not reachable from the utility world', async ({ page }) => {
      await page.exposeFunction('f', (x: number) => x * 2);
      expect(await page.evaluate(() => window.f?.(21) ?? 'NOT PRESENT')).toBe(42);
      // Received: "NOT PRESENT"
      expect(await page.evaluate(() => window.f?.(21) ?? 'NOT PRESENT', undefined, { world: 'utility' })).toBe(42);
    });
    What I ran
    • version: tip-of-tree 1.64.0-next, sha 52a0e72 (checkout built from source); OS: macOS
    • browsers: chromium, firefox, webkit — 9/9 failures, no divergence beyond the engine's wording of the TypeError
    • entry points: page.evaluate, frame.evaluate (page.mainFrame()) and locator.evaluate — all three throw the same callBinding error, as you reported
    • the separate page.exposeFunction + world: 'utility' case fails as an assertion ('NOT PRESENT' instead of 42), i.e. no error, just a missing binding
    • ran as tests/page/ specs against the local build with --retries=0 and with retries; identical every time

    Triaged on macOS by the Playwright bot - agent run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions