CloakHQ/CloakBrowser · error · ElementNotAttachedError

Element ${selector} failed attached check: element not found

Error message

Element ${selector} failed attached check: element not found in DOM

What it means

The actionability probe resolved the selector in the isolated world and got status NOT_FOUND: no element matched the selector at check time. This is the humanized equivalent of Playwright's 'element not attached / not found' failure.

Source

Thrown at js/src/human/actionability.ts:117

  return new Promise(resolve => setTimeout(resolve, BACKOFF_MS[idx]));
}

// ---------------------------------------------------------------------------
// Pre-scroll actionability
// ---------------------------------------------------------------------------

async function stealthActionable(
  pageOrFrame: Page | Frame,
  selector: string,
  checks: ReadonlySet<CheckName>,
): Promise<void> {
  const world = getWorld(pageOrFrame);
  if (!world) throw new StealthWorldUnavailableError();

  const { status, data } = await evalParsed(world, buildActionableJs(selector));
  if (status === UNSUPPORTED) throw new UnsupportedHumanizeSelectorError(selector);
  if (status === EVALUATION_FAILED) throw new StealthEvaluationError(selector);
  if (status === NOT_FOUND) throw new ElementNotAttachedError(selector);
  if (status !== OK || !data) throw new StealthEvaluationError(selector);
  if (checks.has('visible') && !data.visible) throw new ElementNotVisibleError(selector);
  if (checks.has('enabled') && !data.enabled) throw new ElementNotEnabledError(selector);
  if (checks.has('editable') && !data.editable) throw new ElementNotEditableError(selector);
}

async function readBox(
  pageOrFrame: Page | Frame,
  selector: string,
): Promise<{ x: number; y: number; width: number; height: number } | null> {
  const world = getWorld(pageOrFrame);
  if (!world) throw new StealthWorldUnavailableError();

  const { status, data } = await evalParsed(world, buildBoxJs(selector));
  if (status === OK && data?.box) return data.box;
  if (status === NOT_FOUND) return null;
  if (status === UNSUPPORTED) throw new UnsupportedHumanizeSelectorError(selector);
  throw new StealthEvaluationError(selector);

View on GitHub (pinned to d6bad5de26)

Solutions

  1. Wait for the element to exist before the action (waitForSelector / expect(locator).toBeAttached())
  2. Make the selector resilient: prefer stable data-testid or text= selectors over generated CSS paths
  3. If the element genuinely may be absent, catch ElementNotAttachedError and treat it as an expected branch
  4. Verify you are querying the right frame (iframe content needs the Frame object, not the parent Page)

Example fix

// before
await humanClick(page, '#row-42 .btn');
// after
await page.waitForSelector('[data-testid="submit"]');
await humanClick(page, '[data-testid="submit"]');
Defensive patterns

Strategy: validation

Validate before calling

if ((await page.locator(sel).count()) === 0) await page.waitForSelector(sel, { timeout: 5000 });

Type guard

function isElementNotAttachedError(e: unknown): e is ElementNotAttachedError {
  return e instanceof Error && /failed attached check/.test(e.message);
}

Try / catch

try { await humanClick(page, sel); } catch (e) { if (isElementNotAttachedError(e)) return; /* optional flow */ throw e; }

Prevention

When it happens

Trigger: ensureActionable with any check set (visible/enabled/editable) on a selector whose resolver returns NOT_FOUND — element removed from DOM, typo'd selector, or SPA re-render that swapped nodes between discovery and the probe.

Common situations: Selectors that break after re-renders (data-testid churn), acting before the element mounts, detached nodes after route change, or unsupported selector syntax silently resolving to nothing.

Related errors


AI-assisted analysis of CloakHQ/CloakBrowser@d6bad5de26 (2026-08-28). Data as JSON: /api/errors/9db9fb5f5b8d995a. Report an issue: GitHub.