CloakHQ/CloakBrowser · error · ActionabilityError

Element ${selector} failed timeout check: timeout expired be

Error message

Element ${selector} failed timeout check: timeout expired before first check

What it means

ensureActionable polls stealthActionable until a deadline; if the deadline expires before any check has succeeded (and no more specific error was captured), this generic timeout error is thrown. If a prior iteration recorded a lastError, that error is rethrown instead — so seeing this message means the loop never completed a check with no captured error.

Source

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

export async function ensureActionable(
  pageOrFrame: Page | Frame,
  selector: string,
  checks: ReadonlySet<CheckName>,
  timeout: number = 30000,
  force: boolean = false,
): Promise<void> {
  if (force) return;

  const deadline = Date.now() + timeout;
  let attempt = 0;
  let lastError: Error | null = null;

  while (true) {
    const remainingMs = Math.max(0, deadline - Date.now());
    if (remainingMs <= 0) {
      if (lastError) throw lastError;
      throw new ActionabilityError(selector, 'timeout', 'timeout expired before first check');
    }

    try {
      await stealthActionable(pageOrFrame, selector, checks);
      return;
    } catch (error) {
      if (error instanceof ActionabilityError || error instanceof StealthEvaluationError) {
        lastError = error;
        if (Date.now() >= deadline) throw lastError;
        await backoffSleep(attempt++);
      } else {
        throw error;
      }
    }
  }
}

// ---------------------------------------------------------------------------

View on GitHub (pinned to d6bad5de26)

Solutions

  1. Pass a positive timeout matching how long the element realistically takes to become actionable
  2. If you intended an instant check, still pass a small nonzero timeout (e.g. 50-100ms) so at least one check runs
  3. Synchronize on element state before calling the humanized action instead of relying on the internal poll loop
  4. Guard against clock skew in CI containers (sync time, avoid suspended VMs)

Example fix

// before
await humanClick(page, '#btn', { timeout: 0 });
// after
await humanClick(page, '#btn', { timeout: 5000 });
Defensive patterns

Strategy: validation

Validate before calling

const timeoutMs = 5000;
if (!(timeoutMs > 0)) throw new Error('timeout must be positive');

Type guard

function isActionabilityTimeoutError(e: unknown): e is ActionabilityError {
  return e instanceof Error && /failed timeout check/.test(e.message);
}

Try / catch

try { await humanClick(page, sel, { timeout: 5000 }); } catch (e) { if (isActionabilityTimeoutError(e)) { /* fix preconditions, then retry */ } else throw e; }

Prevention

When it happens

Trigger: Passing timeout: 0 (or a deadline already in the past) so remainingMs <= 0 on the first iteration with lastError === null, or a clock/timing edge where the first computed remaining time is non-positive.

Common situations: Explicitly setting timeout to 0 expecting 'no wait' semantics, system clock skew or suspended VMs making Date.now jump forward, or an action scheduled after its own deadline in CI.

Understand the failure class

Related errors


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