{"record":{"id":"6284a64d149db805","repo":"microsoft/playwright","slug":"the-object-has-been-collected-to-prevent-unbounded","errorCode":null,"errorMessage":"The object has been collected to prevent unbounded heap growth.","messagePattern":"The object has been collected to prevent unbounded heap growth\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/playwright-core/src/client/connection.ts","lineNumber":186,"sourceCode":"    return this._objects.get(guid)!;\n  }\n\n  setIsTracing(isTracing: boolean) {\n    if (isTracing)\n      this._tracingCount++;\n    else\n      this._tracingCount--;\n  }\n\n  async sendMessageToServer(object: ChannelOwner, method: string, params: any, options: { apiName?: string, title?: string, internal?: boolean, frames?: channels.StackFrame[], stepId?: string, signal?: AbortSignal, timeout: number }): Promise<any> {\n    // Fire-and-forget: server intentionally never replies to __waitInfo__,\n    // so silently drop it after the connection is closed or the object was collected.\n    if (method === '__waitInfo__' && (this._closedError || object._wasCollected))\n      return;\n    if (this._closedError)\n      throw this._closedError;\n    if (object._wasCollected)\n      throw new Error('The object has been collected to prevent unbounded heap growth.');\n\n    const signal = options.signal;\n    if (signal?.aborted)\n      throw new AbortError(undefined, { cause: signal.reason });\n\n    const guid = object._guid;\n    const type = object._type;\n    const id = ++this._lastId;\n    const message = { id, guid, method, params };\n    if (debugLogger.isEnabled('channel')) {\n      // Do not include metadata in debug logs to avoid noise.\n      debugLogger.log('channel', 'SEND> ' + JSON.stringify(message));\n    }\n    const location = options.frames?.[0] ? { file: options.frames[0].file, line: options.frames[0].line, column: options.frames[0].column } : undefined;\n    const metadata: channels.Metadata = { title: options.title, location, internal: options.internal, stepId: options.stepId, timeout: options.timeout };\n    if (this._tracingCount && options.frames && type !== 'LocalUtils')\n      this._localUtils?.addStackToTracingNoReply({ callData: { stack: options.frames ?? [], id } }).catch(() => {});\n    // We need to exit zones before calling into the server, otherwise","sourceCodeStart":168,"sourceCodeEnd":204,"githubUrl":"https://github.com/microsoft/playwright/blob/c8fc3bf8d31542d59b4d4d9eaab1df93ff541dc6/packages/playwright-core/src/client/connection.ts#L168-L204","documentation":"Thrown in sendMessageToServer when object._wasCollected is true — i.e. the ChannelOwner was disposed with reason 'gc' on the client side (see channelOwner._dispose: _wasCollected = reason === 'gc'). Playwright periodically garbage-collects server objects the client no longer strongly references, to cap heap growth. Calling any method on such a stale handle fails here. Note __waitInfo__ is special-cased to be silently dropped when the object is collected.","triggerScenarios":"Holding a long-lived reference (ElementHandle, JSHandle, Frame, Request, Response, etc.) past its actual GC window, then invoking a method on it (e.g. handle.click(), response.body()). Common in scrapers that cache handles across navigation/reloads, in tests that retain elements from a previous page state, or after the page navigated/closed and the server reclaimed the object.","commonSituations":"Storing element handles in arrays across awaits/loops and reusing them after a navigation; re-querying through a stale Locator's internal handle; long-running daemon that holds references too long; closing the page then touching a saved handle.","solutions":["Re-acquire the handle/locator right before use (prefer Locators, which re-query each call) instead of caching handles.","Scope handles tightly: obtain, use, and discard within one logical operation.","Keep the page/context alive for as long as you hold references derived from them.","If you genuinely need long-lived references, prevent GC by keeping them reachable on the client (but expect higher memory use)."],"exampleFix":"// before\nconst handle = await page.$('#item');\nawait page.click('#reload');\nawait handle.click(); // _wasCollected === true\n\n// after — use a Locator, re-queried per call\nconst item = page.locator('#item');\nawait page.click('#reload');\nawait item.click();","handlingStrategy":"validation","validationCode":"// Guard any cached handle before use. _wasCollected is private; emulate with a reachability check\n// via the public API: re-resolve through a Locator, or call a cheap method and catch.\nasync function isHandleAlive(handle) {\n  try { await handle.waitForElementState('stable', { timeout: 250 }).catch(() => {}); return true; }\n  catch { return false; }\n}\nif (!(await isHandleAlive(handle))) throw new Error('handle collected; re-query');","typeGuard":"import type { ElementHandle, Locator } from 'playwright-core';\n\n// Prefer Locators — they never go stale because they re-query per call.\nfunction asLocator(page: any, selector: string): Locator {\n  return page.locator(selector);\n}\n\n// If you must accept a handle, treat it as possibly-collected at the type level:\ntype MaybeStale<T> = T & { __maybeStale?: true };","tryCatchPattern":"try {\n  await handle.click();\n} catch (e) {\n  if (/collected to prevent unbounded heap growth/.test(String(e?.message))) {\n    // Re-acquire via selector and retry once.\n    await page.locator(selector).click();\n  } else throw e;\n}","preventionTips":["Default to Locators over ElementHandle for anything kept across awaits.","Never cache handles across navigation, reload, or route changes.","Tear down handles you no longer need so GC is predictable."],"tags":["memory","lifecycle","element-handle","garbage-collection"],"backgroundTag":null,"analyzedSha":"c8fc3bf8d31542d59b4d4d9eaab1df93ff541dc6","analyzedAt":"2026-08-12T07:26:36.950Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}