apify/crawlee · error · Error

Provided browserPlugin is not one of the plugins used by Bro

Error message

Provided browserPlugin is not one of the plugins used by BrowserPool.

What it means

newPage() accepts an optional browserPlugin to select which plugin serves the page. Since the pool only knows the plugins it was constructed with, it throws this plain Error when the supplied plugin is not one of the pool's registered browserPlugins (identity check via includes).

Source

Thrown at packages/browser-pool/src/browser-pool.ts:473

     * properties are consumed — cookie and header injection remain the
     * crawler's responsibility.
     */
    async newPage(options: BrowserPoolNewPageOptions<PageOptions, BrowserPlugins[number]> = {}): Promise<PageReturn> {
        const {
            id = nanoid(),
            pageOptions,
            browserPlugin = this.pickBrowserPlugin(),
            session,
            proxyUrl = session?.proxyInfo?.url,
            ignoreTlsErrors = session?.proxyInfo?.ignoreTlsErrors,
        } = options;

        if (this.pages.has(id)) {
            throw new Error(`Page with ID: ${id} already exists.`);
        }

        if (browserPlugin && !this.browserPlugins.includes(browserPlugin)) {
            throw new Error('Provided browserPlugin is not one of the plugins used by BrowserPool.');
        }

        // Bind the limiter callback to the current async-hooks context. p-limit
        // otherwise resumes queued callbacks in the previous task's
        // AsyncLocalStorage context, leaking aborted cancelTasks across unrelated
        // requests (https://github.com/apify/crawlee/issues/3670). Mirrors the
        // fix p-limit landed upstream in v5 (sindresorhus/p-limit#71); v5 is an
        // ESM-only rewrite, so we can't bump it in Crawlee v3.
        // Besides the cancelTask leak, the wrapper also keeps the per-request *storage transaction*
        // ALS-scoped: without it, a queued callback would resume in the previous request's async
        // context and run request B's storage writes inside request A's transaction.
        // TODO(crawlee@v4): bump p-limit to v5 and drop this AsyncResource.bind wrapper.
        // Limiter is necessary - https://github.com/apify/crawlee/issues/1126
        return this.#limiter(
            AsyncResource.bind(async () => {
                let browserController = this.pickBrowserWithFreeCapacity(browserPlugin, { proxyUrl });

                if (!browserController)

View on GitHub (pinned to dbe57fb09c)

Solutions

  1. Pass one of the exact plugin instances given to the BrowserPool constructor (keep references handy).
  2. Register the plugin in the pool's browserPlugins array before using it.
  3. Check membership first: pool.browserPlugins.includes(plugin).

Example fix

// before
const plugin = new PlaywrightPlugin(); // different instance than pool's
await pool.newPage({ browserPlugin: plugin }); // throws
// after
const plugin = pool.browserPlugins[0];
await pool.newPage({ browserPlugin: plugin });
Defensive patterns

Strategy: validation

Validate before calling

if (!pool.browserPlugins.includes(plugin)) throw new Error('register this plugin in the pool first');
await pool.newPage({ browserPlugin: plugin });

Type guard

function isPoolPlugin(pool, plugin) { return pool.browserPlugins.includes(plugin); }

Prevention

When it happens

Trigger: pool.newPage({ browserPlugin: somePlugin }) where somePlugin was never passed to the BrowserPool constructor — e.g. a plugin created in another module, from a different package copy, or a plugin of a different family.

Common situations: Creating plugins in multiple places and mixing instances; duplicate package versions making the seemingly-same plugin a different instance; passing a plugin from one pool to a different pool.

Related errors


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