microsoft/playwright · error · NonRecoverableDOMError

Pierce-frame mode matched elements from multiple frames.

Error message

Pierce-frame mode matched elements from multiple frames.

What it means

In pierce-frames mode the resolver may fan out across multiple iframes. If the selector matches elements in more than one frame, the result is ambiguous, so Playwright throws NonRecoverableDOMError ('Pierce-frame mode matched elements from multiple frames.') rather than silently picking one.

Source

Thrown at packages/playwright-core/src/server/frameSelectors.ts:331

          return func({ injected, elements, info: params.info }, params.arg);
        }, { info, scope, functionText: String(pageFunction), arg, callWithoutMatches: options.callWithoutMatches, markTargets: options.markTargets, returnByValue });
        if (returnByValue && evalResult === '--playwright--no--result--value--')
          return;
        if (!returnByValue && (evalResult as JSHandle)._value === '--playwright--no--result--value--') {
          (evalResult as JSHandle).dispose();
          return;
        }
        return { result: evalResult as R | SmartHandle<R> };
      };
      const maybeResult = noStall ? await frame.raceAgainstEvaluationStallingEvents(getResult).catch(e => {
        if (e instanceof EvaluationStalledError)
          return;
        throw e;
      }) : await getResult();
      if (!maybeResult)
        continue;
      if (aggregatedResult)
        throw new NonRecoverableDOMError(`Pierce-frame mode matched elements from multiple frames.`);
      aggregatedResult = { frame, info, result: maybeResult.result };
    }
    return aggregatedResult;
  }

  async callOnSelector<Arg, R>(
    selector: string,
    options: types.StrictOptions & { mainWorld?: boolean, callWithoutMatches?: boolean, scope?: ElementHandle, markTargets?: 'all' | 'first' | 'none', noDefaultPierce?: boolean },
    pageFunction: MatchedElementsCallback<Arg, R>,
    arg: Arg,
  ): Promise<{ frame: Frame, info: SelectorInfo, result: R } | null> {
    const result = await this._callOnSelectorInternal(selector, options, pageFunction, arg, true /* returnByValue */);
    return result as { frame: Frame, info: SelectorInfo, result: R } | null;
  }

  async callOnSelectorHandle<Arg, R>(
    selector: string,
    options: types.StrictOptions & { mainWorld?: boolean, scope?: ElementHandle, markTargets?: 'all' | 'first' | 'none' },

View on GitHub (pinned to c8fc3bf8d3)

Solutions

  1. Make the selector more specific so it matches in only one frame.
  2. Target the desired frame explicitly via frameLocator rather than relying on piercing.
  3. Scope the query to a particular iframe element handle before querying inside.
  4. Disable pierceFrames and resolve frames manually.

Example fix

// before
await page.locator('button#go').click(); // matches 2 iframes -> throws
// after: target one frame
await page.frameLocator('iframe[name=main]').locator('button#go').click();
Defensive patterns

Strategy: validation

Validate before calling

// Pre-count matching frames to surface ambiguity early
const frames = page.frames().filter(f => await f.locator('button#go').count() > 0);
if (frames.length > 1) throw new Error('selector matches multiple frames');

Type guard

null

Try / catch

null

Prevention

When it happens

Trigger: Using a piercing locator (pierceFrames on or a piercing selector) where the same selector text matches elements inside two or more iframes in the subtree.

Common situations: Pages with multiple iframes that happen to contain the same component/selector (e.g. repeated ad slots, duplicated widgets); tests that worked until a second iframe with the same content was added.

Related errors


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