Skip to content

fix: prevent page.evaluate promises from being collected - #2751

Open
Oxygen56 wants to merge 2 commits into
browserbase:mainfrom
Oxygen56:fix/2745-page-evaluate-promise-lifetime
Open

fix: prevent page.evaluate promises from being collected#2751
Oxygen56 wants to merge 2 commits into
browserbase:mainfrom
Oxygen56:fix/2745-page-evaluate-promise-lifetime

Conversation

@Oxygen56

@Oxygen56 Oxygen56 commented Aug 17, 2026

Copy link
Copy Markdown

why

Closes #2745.

Runtime.evaluate previously awaited an evaluation result before retaining a remote object handle. During garbage collection and page churn, Chrome could collect that promise and fail the command with Promise was collected.

what changed

  • Evaluate by reference first, then await returned promises through their retained object ID.
  • Serialize synchronous object results through the same retained handle and release every handle after use.
  • Add regression coverage for promise, object, primitive, and cleanup behavior.
  • Rebuild the deterministic extension archive embedded by the Go SDK.
  • Add the required patch changeset for the extension and SDKs that embed it.

test plan

  • Direct Chrome before/after regression, repeated five times: under the same forced garbage collection, the old direct-await strategy fails with Runtime.evaluate -32000 Promise was collected, while the retained-handle strategy resolves the same controllable promise to the expected value.
  • Direct Chrome behavior comparison across 14 synchronous, asynchronous, and exceptional result shapes; no differences found.
  • Patched extension smoke loop: 80 pages created, navigated, evaluated, and closed; 320 synchronous and promised evaluations returned the expected values with no failures.
  • Focused regression suite: 4 tests passed.
  • Extension unit suite: 337 tests passed, with 10 existing todo cases.
  • Repository formatting, changed-file lint, and the applicable TypeScript typecheck.
  • Extension production build and embedded Go archive drift check.
  • Affected Go package tests and build.
  • git diff --check.
  • The unrelated Go generator suite requires generator-only modules that are not present offline and remains delegated to CI.

Summary by cubic

Prevents Chrome from collecting Page.evaluate results during GC by retaining the remote handle before awaiting. Old behavior awaited in Runtime.evaluate and sometimes failed with “Promise was collected”; new behavior evaluates by reference, then materializes via the retained object, reducing flakiness.

  • Materializes results through CDP:
    • Promises: Runtime.awaitPromise on the returned objectId, then Runtime.releaseObject.
    • Synchronous objects: Runtime.callFunctionOn to return-by-value, then Runtime.releaseObject.
    • Primitives: unchanged path, return-by-value directly.
  • Improves error messages by favoring exception.description when present.
  • Refactors Page.evaluate to delegate to frame-level evaluation for consistency.
  • Adds regression tests for promise/object/primitive paths and cleanup.
  • Rebuilds the deterministic @browserbasehq/stagehand-extension archive and adds patch changesets for @browserbasehq/stagehand, @browserbasehq/stagehand-python, and @browserbasehq/stagehand-go to ship the fix.

Written for commit d1f487c. Summary will update on new commits.

Review in cubic

@changeset-bot

changeset-bot Bot commented Aug 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d1f487c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 11 packages
Name Type
@browserbasehq/stagehand Patch
@browserbasehq/stagehand-python Patch
@browserbasehq/stagehand-go Patch
@browserbasehq/stagehand-extension Patch
@browserbasehq/stagehand-integrations Patch
@browserbasehq/stagehand-integrations-example-eve-facade Patch
@browserbasehq/stagehand-integrations-example-pi-facade Patch
@browserbasehq/stagehand-integrations-example-claude-code-facade Patch
@browserbasehq/stagehand-integrations-example-codex-facade Patch
@browserbasehq/stagehand-integrations-example-mastra-facade Patch
@browserbasehq/stagehand-integrations-example-vercel-ai-facade Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 5 files

Confidence score: 3/5

  • In packages/extension/understudy/frame.ts (Frame.evaluate), throwing a generic Error with raw exception details can leak internal/runtime messages through Page.evaluate, which creates a user-facing error-sanitization risk — switch to a typed error and strip/redact the raw exception text before rethrowing.
  • In packages/extension/understudy/frame.ts (Frame.evaluate), the new Runtime.awaitPromise/Runtime.callFunctionOn materialization path runs outside the existing retry guard for context-loss errors, so transient context invalidation may now surface as flaky evaluation failures instead of being retried — extend the same "Cannot find context with specified id" retry handling to this post-evaluate step.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/extension/understudy/frame.ts">

<violation number="1" location="packages/extension/understudy/frame.ts:181">
P2: The new materialization step (`Runtime.awaitPromise`/`Runtime.callFunctionOn`) runs against the retained `objectId` outside the try/catch that retries `Runtime.evaluate` on "Cannot find context with specified id". When page churn recreates the context between the evaluate and this follow-up call, the follow-up fails with no retry, so the whole `evaluate` rejects. Consider wrapping the materialize step with the same context-recreation retry (re-running evaluate with a fresh context id) or documenting why a retry is intentionally omitted for the non-evaluate round-trips.</violation>

<violation number="2" location="packages/extension/understudy/frame.ts:183">
P2: Custom agent: **Exception and error message sanitization**

`Frame.evaluate` throws a generic `new Error(...)` that bubbles to users through `Page.evaluate`. Replace it with a typed error class and remove the raw `exceptionDetails.exception?.description` fallback, which surfaces unsanitized JavaScript exception text to the user.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

returnByValue: false,
});
}
res = await this.materializeEvaluationResult(res);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The new materialization step (Runtime.awaitPromise/Runtime.callFunctionOn) runs against the retained objectId outside the try/catch that retries Runtime.evaluate on "Cannot find context with specified id". When page churn recreates the context between the evaluate and this follow-up call, the follow-up fails with no retry, so the whole evaluate rejects. Consider wrapping the materialize step with the same context-recreation retry (re-running evaluate with a fresh context id) or documenting why a retry is intentionally omitted for the non-evaluate round-trips.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/extension/understudy/frame.ts, line 181:

<comment>The new materialization step (`Runtime.awaitPromise`/`Runtime.callFunctionOn`) runs against the retained `objectId` outside the try/catch that retries `Runtime.evaluate` on "Cannot find context with specified id". When page churn recreates the context between the evaluate and this follow-up call, the follow-up fails with no retry, so the whole `evaluate` rejects. Consider wrapping the materialize step with the same context-recreation retry (re-running evaluate with a fresh context id) or documenting why a retry is intentionally omitted for the non-evaluate round-trips.</comment>

<file context>
@@ -170,16 +174,47 @@ export class Frame implements FrameManager {
+        returnByValue: false,
       });
     }
+    res = await this.materializeEvaluationResult(res);
     if (res.exceptionDetails) {
-      throw new Error(res.exceptionDetails.text ?? "Evaluation failed");
</file context>

res = await this.materializeEvaluationResult(res);
if (res.exceptionDetails) {
throw new Error(res.exceptionDetails.text ?? "Evaluation failed");
throw new Error(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Custom agent: Exception and error message sanitization

Frame.evaluate throws a generic new Error(...) that bubbles to users through Page.evaluate. Replace it with a typed error class and remove the raw exceptionDetails.exception?.description fallback, which surfaces unsanitized JavaScript exception text to the user.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/extension/understudy/frame.ts, line 183:

<comment>`Frame.evaluate` throws a generic `new Error(...)` that bubbles to users through `Page.evaluate`. Replace it with a typed error class and remove the raw `exceptionDetails.exception?.description` fallback, which surfaces unsanitized JavaScript exception text to the user.</comment>

<file context>
@@ -170,16 +174,47 @@ export class Frame implements FrameManager {
+    res = await this.materializeEvaluationResult(res);
     if (res.exceptionDetails) {
-      throw new Error(res.exceptionDetails.text ?? "Evaluation failed");
+      throw new Error(
+        res.exceptionDetails.text ||
+          res.exceptionDetails.exception?.description ||
</file context>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

page.evaluate intermittently fails with "Promise was collected" when pages are created and closed in a loop

1 participant