apify/crawlee · error · Error
Page with ID: ${id} already exists.
Error message
Page with ID: ${id} already exists. What it means
newPage() generates or accepts a page id and tracks in-flight page creation promises in this.pages to prevent duplicate concurrent creation under the same id. This plain Error is thrown when the given (or default nanoid) id already has an entry, because two pages with the same id would corrupt the pool's page bookkeeping.
Source
Thrown at packages/browser-pool/src/browser-pool.ts:469
* for `proxyUrl` and `ignoreTlsErrors` respectively. Explicit `proxyUrl` /
* `ignoreTlsErrors` values in the options take precedence.
*
* Beyond fingerprint caching and proxy configuration, no other session
* 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(View on GitHub (pinned to dbe57fb09c)
Solutions
- Use unique ids per newPage call (omit `id` to let nanoid generate one).
- Await the previous page promise before creating another page with the same id.
- Catch the error and generate a fresh id, or check `pool.pages.has(id)` before calling.
Example fix
// before
await pool.newPage({ id: requestId });
await pool.newPage({ id: requestId }); // throws
// after
const id = pool.pages.has(requestId) ? `${requestId}-${nanoid()}` : requestId;
await pool.newPage({ id }); Defensive patterns
Strategy: validation
Validate before calling
if (pool.pages.has(id)) id = `${id}-${nanoid()}`;
await pool.newPage({ id }); Try / catch
try {
await pool.newPage({ id });
} catch (err) {
if (err.message.startsWith('Page with ID:')) {
await pool.newPage({ id: `${id}-${nanoid()}` });
} else throw err;
} Prevention
- Omit `id` to auto-generate a unique one
- Never reuse ids without awaiting the previous page promise
- Map request keys to unique page ids via nanoid suffix
When it happens
Trigger: Calling pool.newPage({ id: 'same-id' }) (or page()/pagePromises() which delegate to newPage) twice with the same id while the first page is still being created or tracked; reusing an id after an earlier newPage that hasn't completed/cleaned up.
Common situations: Retry logic that reuses a request/page id without waiting for the first attempt; request-key collisions when mapping crawler requests to page ids; creating pages in a loop with a constant id.
Related errors
- Cannot extend LaunchContext with key: ${key}, because it's r
- ${crawlerName}: ${names.map((name) => `\`${name}\``).join(',
- Cannot activate BrowserController without an assigned browse
- BrowserController already has a browser instance assigned.
- Provided browserPlugin is not one of the plugins used by Bro
AI-assisted analysis of apify/crawlee@dbe57fb09c (2026-08-30).
Data as JSON: /api/errors/166a74da05b5e1f4.
Report an issue: GitHub.