microsoft/playwright · error · InvalidSelectorError
nth can only be the last locator when piercing frames…
Error message
nth can only be the last locator when piercing frames, while querying "${locator}" What it means
In pierce-frames mode, the nth pseudo-part (>> nth=N) is only valid as the final part of a selector. If nth appears anywhere except the last position, InvalidSelectorError is thrown because the piercing resolver cannot reorder it.
Solutions
- Move nth to the end of the locator string.
- Re-record/re-generate the locator so nth is last.
- Wrap with frameLocator/locator API and call .first()/.nth() as a method on the final locator instead of an inline token.
Example fix
// before
page.locator('iframe >> button >> nth=0 >> span'); // nth not last
// after
page.frameLocator('iframe').locator('button').first().locator('span'); Defensive patterns
Strategy: validation
Validate before calling
// Ensure nth is only the last token when piercing
const parts = sel.split('>>').map(s => s.trim());
const nthIdx = parts.findIndex(p => p.startsWith('nth='));
if (nthIdx !== -1 && nthIdx !== parts.length - 1) throw new Error('nth must be last'); Type guard
null
Try / catch
null
Prevention
- Use .first()/.nth() methods on the Locator object rather than inline nth tokens.
- Re-validate locators after enabling pierceFrames.
When it happens
Trigger: Using a piercing locator whose nth component is not last, e.g. locator('iframe >> button >> nth=0 >> span') with pierceFrames on, or chaining after nth.
Common situations: Authoring locators by hand with nth in the middle; generated locators from recorder placing nth before trailing parts; enabling piercing on existing nth-bearing locators.
Related errors
- Composite locators are not supported with piercing frames…
- Can not *-capture inside a frame-piercing selector, while…
- Can not capture the selector before diving into the frame…
- Conflicting exactness in internal:role selector
- Error while parsing css selector
AI-assisted analysis of microsoft/playwright@500c9c822c (2026-08-12).
Data as JSON: /api/errors/3354383261fc402b.
Report an issue: GitHub.
Appendix: source
Thrown at packages/playwright-core/src/server/frameSelectors.ts:155
for (const chunk of chunks) {
visitAllSelectorParts(chunk, (part, nested) => {
if (nested && part.name === 'internal:control' && part.body === 'enter-frame') {
const locator = asLocator(this.frame._page.browserContext._browser.sdkLanguage(), selector);
throw new InvalidSelectorError(`Frame locators are not allowed inside composite locators, while querying "${locator}"`);
}
if (nested && part.name === 'internal:control' && part.body === 'any-frame') {
const locator = asLocator(this.frame._page.browserContext._browser.sdkLanguage(), selector);
throw new InvalidSelectorError(`frameLocator() is not allowed inside composite locators, while querying "${locator}"`);
}
});
}
if (!anyFrame && !options.anyFrame) {
const result = await this._resolveChainedSelector(this.frame, selector, options, chunks, scope, false /* noStall */);
return result ? [result] : [];
}
const result: SelectorInFrame[] = [];
const seenFrames = new Set<Frame>();
for (const startFrame of await this._anyFrameCandidates(scope)) {
const resolved = await this._resolveChainedSelector(startFrame, selector, options, chunks, startFrame === this.frame ? scope : undefined, startFrame !== this.frame /* noStall */);
// Different starting frames may resolve into the same frame, e.g. with an aria-ref selector.
if (!resolved || seenFrames.has(resolved.frame))
continue;
seenFrames.add(resolved.frame);
result.push(resolved);
}
return result;
}
// All the frames the selector may start in: the frame subtree, restricted to the scope when given.
private async _anyFrameCandidates(scope: ElementHandle | undefined): Promise<Frame[]> {
const result: Frame[] = [];
const collectSubtree = (frame: Frame) => {
result.push(frame);
for (const child of frame.childFrames())View on GitHub (pinned to 500c9c822c)