{"record":{"id":"9d44a14f164d2bfd","repo":"can1357/oh-my-pi","slug":"label-returned-a-promise-but-this-surface-eval","errorCode":null,"errorMessage":"${label} returned a Promise, but this surface evaluates synchronously and cannot await it — return a plain value (poll with waitForFunction for async state instead)","messagePattern":"(.+?) returned a Promise, but this surface evaluates synchronously and cannot await it — return a plain value \\(poll with waitForFunction for async state instead\\)","errorType":"exception","errorClass":"ToolError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/tools/browser/cmux/rpc.ts","lineNumber":163,"sourceCode":"\t\t} catch (e) {\n\t\t\treturn { __ompErr: (e && (e.stack || e.message)) || String(e) };\n\t\t}\n\t})()`;\n}\n\n/**\n * Decode a {@link serializeEvalWithEnvelope} result: rethrow page-side\n * exceptions as rich {@link ToolError}s, reject unserializable Promise\n * returns with an actionable message, and pass through values from daemons\n * that did not run the wrapper.\n */\nexport function unwrapEvalEnvelope<TResult>(value: unknown, label: string): TResult {\n\tif (value && typeof value === \"object\") {\n\t\tif (\"__ompErr\" in value && typeof value.__ompErr === \"string\") {\n\t\t\tthrow new ToolError(`${label} threw a JavaScript exception:\\n${value.__ompErr}`);\n\t\t}\n\t\tif (\"__ompPromise\" in value && value.__ompPromise === true) {\n\t\t\tthrow new ToolError(\n\t\t\t\t`${label} returned a Promise, but this surface evaluates synchronously and cannot await it — return a plain value (poll with waitForFunction for async state instead)`,\n\t\t\t);\n\t\t}\n\t\tif (\"__ompOk\" in value) {\n\t\t\treturn value.__ompOk as TResult;\n\t\t}\n\t}\n\treturn value as TResult;\n}\n\nexport function mapWaitUntil(waitUntil: string | undefined): \"interactive\" | \"complete\" {\n\treturn waitUntil === \"domcontentloaded\" ? \"interactive\" : \"complete\";\n}\n\nexport interface ResolveCmuxKindOptions {\n\tsurface?: string;\n\tsettingEnabled?: boolean;\n}","sourceCodeStart":145,"sourceCodeEnd":181,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/tools/browser/cmux/rpc.ts#L145-L181","documentation":"The cmux browser eval wrapper runs page scripts through a synchronous envelope (serializeEvalWithEnvelope). When the evaluated function returns a Promise, the daemon cannot serialize/await it, so the envelope flags it with __ompPromise and unwrapEvalEnvelope converts that flag into this ToolError explaining the surface is synchronous. It exists to turn an opaque 'unsupported type' daemon failure into an actionable message.","triggerScenarios":"Calling evaluate() or evaluateOnSelector() with a function/expression whose return value is a Promise — e.g. `async () => { ... }`, `() => fetch('/api')`, or `() => document.querySelector('x').asyncMethod()`. The wrapper detects `typeof __v.then === 'function'` and flags it before any serialization is attempted.","commonSituations":"Writing eval scripts as async functions out of habit from Puppeteer/Playwright where evaluate awaits promises; calling browser fetch() or storage APIs inside eval; wrapping an async helper to read DOM state that is actually available synchronously.","solutions":["Remove async/await and return a plain synchronous value from the eval function (DOM reads like textContent/getBoundingClientRect are synchronous)","If you need data from an async operation (fetch, storage), poll for it from the host with waitForFunction instead of awaiting inside eval","Call .then-free synchronous accessors inside eval and compose async steps as multiple separate evaluate calls"],"exampleFix":"// before\nconst title = await browser.evaluate(async () => {\n  const res = await fetch('/api/title');\n  return (await res.json()).title;\n});\n// after\nawait browser.waitForFunction(() => document.querySelector('#title') !== null);\nconst title = await browser.evaluate(() => document.querySelector('#title').textContent);","handlingStrategy":"validation","validationCode":"function returnsPromise(fn) {\n  try {\n    return fn() && typeof fn().then === 'function';\n  } catch { return false; }\n}\n// before calling evaluate: if (returnsPromise(myFn)) rewrite to a sync function","typeGuard":"function isEvalEnvelope(v: unknown): v is { __ompOk?: unknown; __ompErr?: string; __ompPromise?: boolean } {\n  return typeof v === 'object' && v !== null &&\n    ('__ompOk' in v || '__ompErr' in v || '__ompPromise' in v);\n}","tryCatchPattern":"try {\n  const value = unwrapEvalEnvelope(result, 'evaluate');\n} catch (err) {\n  if (err instanceof ToolError && err.message.includes('returned a Promise')) {\n    // rewrite script to sync form or switch to waitForFunction polling\n  }\n  throw err;\n}","preventionTips":["Never write async eval functions for cmux surfaces — DOM state reads are synchronous","Use waitForFunction on the host side for anything requiring eventual consistency","Avoid fetch()/Promise-based APIs inside evaluate; do them outside the browser surface","Review eval snippets for arrow functions marked async before submitting"],"tags":["browser","eval","async","promise"],"backgroundTag":"sync-eval-returned-promise","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}