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

  1. Use one of: attached|detached|visible|hidden.
  2. Move lifecycle waits (load/domcontentloaded/networkidle) to page.waitForLoadState.
  3. 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

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


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