#371·ego-lite

[Bug]: Nested function returns make evaluate()/js() silently return null

Author: chitacdcolorrrrCreated Sep 7, 2026Updated Sep 19, 2026

Affected component

JavaScript string evaluation in the open helper runtime: page.evaluate() on main/dev, and the legacy js() helper in the current app/beta runtime.

What happened?

A valid expression containing a return inside a nested function silently loses its result:

javascript
// main/dev facade:
await page.evaluate('[1, 2].map(x => { return x * 2; })')
// Expected: [2, 4]
// Actual: null

// Installed app's legacy helper:
await js('[1, 2].map(x => { return x * 2; })')
// Also returns null.

This affects common page extraction expressions such as .map(el => { return el.textContent; }). Function declarations and methods can trigger the same failure. A control, return 42, correctly returns 42.

Root cause

src/cdp-eval.ts scans for the return word without tracking function scope. evaluate()/js() then wraps the entire expression in an IIFE with no outer return:

javascript
(function () {
  [1, 2].map(x => { return x * 2; })
})()

The result is undefined, which runtimeValue() converts to null. Conversely, the current startsWith('(') exception misses a real outer return in (1 + 2); return 4; and sends an illegal top-level return to the browser.

Related: #218 fixes regex-literal handling in this scanner, but does not distinguish nested function returns from outer returns. The scope bug reported here remains after that proposed change.

Reproduction and verification boundary

  • main: 5ca3c36cba2240b8df2e22ba32127747029039d5.
  • dev (patch base): ca746891e208d3a8ca8a20f4ca91eed4ac53c64f.
  • The same scanner and wrapping condition remain in 2.0.0-beta-dev at 8ac6e585ee32a5d463368f0ca0ccf3958c165945; that branch exposes this path as js().
  • Reproduced in the installed ego lite app on macOS 26.5.2, using an isolated data-URL test page and the standard ego-browser nodejs entry point. Both the .map() expression above and function f() { return 3; } f() returned null.
  • Loaded the patched checkout through ego-browser nodejs --sdk-path <absolute-bundle-path> in the same task space. The nested callback, function declaration, regex literal, parenthesized prefix plus outer return, and plain outer return all produced their expected values.

Fix available

Commit 57be7da / downloadable patch, based on dev.

  • Reuse the existing Acorn dependency to parse the source.
  • Detect outer ReturnStatement nodes while skipping nested functions.
  • Remove the parenthesis shortcut; real IIFEs already have only nested returns.
  • Leave syntax the parser cannot accept unchanged for the browser to report.
  • Add regression tests that execute the emitted JavaScript in Node's VM, rather than making a CDP mock return the expected value unconditionally.

Validation: the new regression coverage produced nine failing checks before the fix; all 43 evaluation tests and the full 322-test suite pass after it. Typecheck, changed-file Prettier, git diff --check, and site-skill validation pass. Five focused real-Ego checks pass with the patched bundle. The full browser E2E suite was not run.

The public helper signatures and dependencies are unchanged. Upstream currently allows only collaborators to create pull requests, so the tested fix is supplied as a fork commit for review/application.