microsoft/playwright · error · Error
state: expected one of (attached|detached|visible|hidden)
Error message
state: expected one of (attached|detached|visible|hidden)
What it means
waitForSelector validates that options.state is one of 'attached', 'detached', 'visible', 'hidden'. Any other value throws 'state: expected one of (attached|detached|visible|hidden)'.
Source
Thrown at packages/playwright-core/src/server/frames.ts:845
private async _evaluateExpressionHandle(expression: string, options: { isFunction?: boolean, world?: types.World } = {}, arg?: any): Promise<js.JSHandle<any>> {
const context = await this.context(options.world ?? 'main');
const value = await context.evaluateExpressionHandle(expression, options, arg);
return value;
}
async querySelector(progress: Progress, selector: string, options: types.StrictOptions): Promise<dom.ElementHandle<Element> | null> {
this.apiLog(` finding element using the selector "${selector}"`);
return progress.race(this.selectors.query(selector, options));
}
async waitForSelector(progress: Progress, selector: string, performActionPreChecksAndLog: boolean, options: types.WaitForElementOptions, scope?: dom.ElementHandle): Promise<dom.ElementHandle<Element> | null> {
if ((options as any).visibility)
throw new Error('options.visibility is not supported, did you mean options.state?');
if ((options as any).waitFor && (options as any).waitFor !== 'visible')
throw new Error('options.waitFor is not supported, did you mean options.state?');
const { state = 'visible' } = options;
if (!['attached', 'detached', 'visible', 'hidden'].includes(state))
throw new Error(`state: expected one of (attached|detached|visible|hidden)`);
if (performActionPreChecksAndLog)
progress.log(`waiting for ${this._asLocator(selector)}${state === 'attached' ? '' : ' to be ' + state}`);
const promise = this.retryWithProgressAndBackoff(progress, async (progress, continuePolling) => {
if (performActionPreChecksAndLog)
await this._page.performActionPreChecks(progress);
if (scope && await progress.race(scope.evaluateInUtility(([injected, node]) => node.isConnected, {})) !== true)
throw new dom.NonRecoverableDOMError('Element is not attached to the DOM');
const resolved = await progress.race(this.selectors.callOnSelectorHandle(selector, { ...options, scope }, ({ injected, elements }) => {
const element: Element | undefined = elements[0];
const visible = element ? injected.utils.isElementVisible(element) : false;
let log = '';
if (elements.length > 1)
log = ` locator resolved to ${elements.length} elements. Proceeding with the first one: ${injected.previewNode(elements[0])}`;
else if (element)
log = ` locator resolved to ${visible ? 'visible' : 'hidden'} ${injected.previewNode(element)}`;
return { log, element, visible, attached: !!element };View on GitHub (pinned to c8fc3bf8d3)
Solutions
- Use one of: attached|detached|visible|hidden.
- Move lifecycle waits (load/domcontentloaded/networkidle) to page.waitForLoadState.
- Type the option narrowly (e.g. a string-literal union) to catch typos at compile time.
Example fix
// before
await page.waitForSelector('#x', { state: 'load' }); // throws
// after
await page.waitForSelector('#x', { state: 'visible' });
await page.waitForLoadState('load'); Defensive patterns
Strategy: validation
Validate before calling
const VALID = ['attached','detached','visible','hidden'] as const;
if (!VALID.includes(opts.state)) throw new Error(`state must be one of ${VALID.join('|')}`); Type guard
type State = 'attached'|'detached'|'visible'|'hidden';
function isValidState(s: string): s is State {
return ['attached','detached','visible','hidden'].includes(s);
} Try / catch
null
Prevention
- Type the option as a string-literal union so the compiler rejects bad values.
- Keep element-state and lifecycle-state options in distinct, typed objects.
When it happens
Trigger: Passing an invalid state such as 'load', 'networkidle', 'stable', undefined-as-string, or a typo like 'visble'. Most commonly confusing waitForSelector's state with waitForLoadState's lifecycle values.
Common situations: Migrating between the two wait APIs; auto-generated code passing the wrong enum; typos; passing 'load'/'networkidle' from a shared config object.
Related errors
- Error while parsing selector `${selector}` - selector cannot
- Frame locators are not allowed inside composite locators, wh
- Composite locators are not supported with piercing frames, w
- options.visibility is not supported, did you mean options.st
- options.waitFor is not supported, did you mean options.state
AI-assisted analysis of microsoft/playwright@c8fc3bf8d3 (2026-08-12).
Data as JSON: /api/errors/9f055240ebb77001.
Report an issue: GitHub.