{"record":{"id":"9a4d635b6de0d94b","repo":"stablyai/orca","slug":"webcontents-destroyed","errorCode":null,"errorMessage":"WebContents destroyed","messagePattern":"WebContents destroyed","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/browser/cdp-screenshot.ts","lineNumber":138,"sourceCode":"    return await Promise.race([\n      webContents.debugger.sendCommand(method, params ?? {}) as Promise<T>,\n      new Promise<T>((_, reject) => {\n        timer = setTimeout(() => reject(new Error(timeoutMessage)), SCREENSHOT_TIMEOUT_MS)\n      })\n    ])\n  } finally {\n    if (timer) {\n      clearTimeout(timer)\n    }\n  }\n}\n\nexport async function captureFullPageScreenshot(\n  webContents: WebContents,\n  format: 'png' | 'jpeg' = 'png'\n): Promise<{ data: string; format: 'png' | 'jpeg' }> {\n  if (webContents.isDestroyed()) {\n    throw new Error('WebContents destroyed')\n  }\n  const dbg = webContents.debugger\n  if (!dbg.isAttached()) {\n    throw new Error('Debugger not attached')\n  }\n\n  try {\n    webContents.invalidate()\n  } catch {\n    // Some guest teardown paths reject repaint requests. Fall through to CDP.\n  }\n\n  const metrics = await sendCommandWithTimeout<{\n    cssContentSize?: { width?: number; height?: number }\n    contentSize?: { width?: number; height?: number }\n  }>(webContents, 'Page.getLayoutMetrics', undefined, SCREENSHOT_TIMEOUT_MESSAGE)\n  const clip = getLayoutClip(metrics)\n  if (!clip) {","sourceCodeStart":120,"sourceCodeEnd":156,"githubUrl":"https://github.com/stablyai/orca/blob/1136503c6a231a16dce8f921f6fadb63d181e8db/src/main/browser/cdp-screenshot.ts#L120-L156","documentation":"The standalone screenshot module (cdp-screenshot.ts) checks webContents.isDestroyed() before attempting captureFullPageScreenshot(). A destroyed WebContents has no live renderer to paint from, so CDP screenshot commands would fail or hang. Unlike the CdpBridge variants, this throws a plain Error (not a BrowserError with a structured code), so callers must match on the message string or check isDestroyed() themselves.","triggerScenarios":"Calling captureFullPageScreenshot() after the tab was closed, the renderer crashed, or a process swap replaced the WebContents identity.","commonSituations":"Tab closed between the screenshot request and execution; renderer OOM; screenshotting during teardown; stale WebContents reference from a prior navigation.","solutions":["Check webContents.isDestroyed() before calling captureFullPageScreenshot().","Re-acquire a valid WebContents reference from BrowserManager if destroyed.","If the tab is gone, open a new one before retrying."],"exampleFix":"// before\nconst result = await captureFullPageScreenshot(wc)\n\n// after\nif (wc.isDestroyed()) {\n  throw new Error('Cannot screenshot: tab was destroyed')\n}\nconst result = await captureFullPageScreenshot(wc)","handlingStrategy":"validation","validationCode":"if (webContents.isDestroyed()) {\n  throw new Error('Cannot screenshot: WebContents was destroyed')\n}\nconst result = await captureFullPageScreenshot(webContents, format)","typeGuard":"function isLiveWebContents(wc: Electron.WebContents): boolean {\n  return !wc.isDestroyed()\n}","tryCatchPattern":"try {\n  return await captureFullPageScreenshot(wc, format)\n} catch (e) {\n  if (e instanceof Error && e.message === 'WebContents destroyed') {\n    // re-acquire WebContents or report tab-closed to user\n  }\n  throw e\n}","preventionTips":["Check webContents.isDestroyed() before calling captureFullPageScreenshot().","Register a 'destroyed' listener to proactively invalidate references.","Note this throws a plain Error, not a BrowserError — match on message, not code."],"tags":["screenshot","electron","lifecycle","webcontents"],"backgroundTag":null,"analyzedSha":"1136503c6a231a16dce8f921f6fadb63d181e8db","analyzedAt":"2026-08-12T23:15:58.167Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}