apify/crawlee · error

Selector '${selector}' not found.

Error message

Selector '${selector}' not found.

What it means

JSDOMCrawler's waitForSelector is implemented with cheerio over the static body; it polls every 50ms until the timeout expires. If the selector still matches nothing when timeoutMs reaches 0, it throws 'Selector ... not found.' — it never re-fetches the page, so the selector must exist in the already-loaded HTML.

Source

Thrown at packages/jsdom-crawler/src/internals/jsdom-crawler.ts:433

                return addRequests(urls, {
                    ...options,
                    baseUrl,
                    strategy: options.strategy ?? EnqueueStrategy.SameHostname,
                });
            },
            async waitForSelector(selector: string, timeoutMs = 5_000) {
                const cheerio = await import('cheerio');
                const $ = cheerio.load(crawlingContext.body);

                if ($(selector).get().length === 0) {
                    if (timeoutMs) {
                        await sleep(50);
                        await this.waitForSelector(selector, Math.max(timeoutMs - 50, 0));
                        return;
                    }

                    throw new Error(`Selector '${selector}' not found.`);
                }
            },
            async parseWithCheerio(selector?: string, _timeoutMs = 5_000) {
                const cheerio = await import('cheerio');
                const $ = cheerio.load(crawlingContext.body);

                if (selector && $(selector).get().length === 0) {
                    throw new Error(`Selector '${selector}' not found.`);
                }

                return $;
            },
        };
    }
}

/**
 * Extracts URLs from a given Window object.

View on GitHub (pinned to dbe57fb09c)

Solutions

  1. Verify the selector exists in the fetched HTML (log `context.body` or test with cheerio).
  2. Enable `runScripts: true` (or wait for window load) if the element is created by client-side JS.
  3. Increase timeoutMs, but remember the HTML is static — a longer wait only helps if scripts mutate the DOM.
  4. Fix selector typos and confirm the correct case-sensitive attribute syntax.

Example fix

// before
await context.waitForSelector('.dynamic-item', 0); // throws immediately if absent

// after
await context.waitForSelector('.dynamic-item', 5000); // poll up to 5s (JS must populate DOM)
Defensive patterns

Strategy: try-catch

Validate before calling

const $ = (await import('cheerio')).load(context.body);
if ($(selector).get().length === 0) {
    logger.warning(`Selector ${selector} not in served HTML; waitForSelector would exhaust timeout`);
}

Type guard

null

Try / catch

try {
    await context.waitForSelector('.item', 5000);
} catch (err) {
    if ((err as Error).message.includes("not found")) {
        logger.warning('Element never appeared in static HTML', { selector: '.item' });
        return; // skip request or fallback parsing
    }
    throw err;
}

Prevention

When it happens

Trigger: Calling `context.waitForSelector(selector)` with a selector absent from the parsed body, or with `timeoutMs = 0` for an element not present immediately; using selectors that only appear after client-side JS execution not reflected in the served HTML.

Common situations: Waiting for SPA-rendered elements while crawling server-rendered HTML; typos in selectors; elements injected by runScripts but the crawl raced the window load; passing timeout 0 expecting infinite wait.

Related errors


AI-assisted analysis of apify/crawlee@dbe57fb09c (2026-08-30). Data as JSON: /api/errors/44114f2f7b62e4d7. Report an issue: GitHub.