can1357/oh-my-pi · error · ToolError
returned a Promise, but this surface evaluates…
Error message
${label} returned a Promise, but this surface evaluates synchronously and cannot await it — return a plain value (poll with waitForFunction for async state instead) What it means
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.
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
Example fix
// before
const title = await browser.evaluate(async () => {
const res = await fetch('/api/title');
return (await res.json()).title;
});
// after
await browser.waitForFunction(() => document.querySelector('#title') !== null);
const title = await browser.evaluate(() => document.querySelector('#title').textContent); Defensive patterns
Strategy: validation
Validate before calling
function returnsPromise(fn) {
try {
return fn() && typeof fn().then === 'function';
} catch { return false; }
}
// before calling evaluate: if (returnsPromise(myFn)) rewrite to a sync function Type guard
function isEvalEnvelope(v: unknown): v is { __ompOk?: unknown; __ompErr?: string; __ompPromise?: boolean } {
return typeof v === 'object' && v !== null &&
('__ompOk' in v || '__ompErr' in v || '__ompPromise' in v);
} Try / catch
try {
const value = unwrapEvalEnvelope(result, 'evaluate');
} catch (err) {
if (err instanceof ToolError && err.message.includes('returned a Promise')) {
// rewrite script to sync form or switch to waitForFunction polling
}
throw err;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- aborted
- Accessibility snapshot unavailable
- agent() blocked: turn token budget exhausted
- agent() isolated apply failed for
- agent() isolated nested patch apply failed for
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/9d44a14f164d2bfd.
Report an issue: GitHub.
Appendix: source
Thrown at packages/coding-agent/src/tools/browser/cmux/rpc.ts:163
} catch (e) {
return { __ompErr: (e && (e.stack || e.message)) || String(e) };
}
})()`;
}
/**
* Decode a {@link serializeEvalWithEnvelope} result: rethrow page-side
* exceptions as rich {@link ToolError}s, reject unserializable Promise
* returns with an actionable message, and pass through values from daemons
* that did not run the wrapper.
*/
export function unwrapEvalEnvelope<TResult>(value: unknown, label: string): TResult {
if (value && typeof value === "object") {
if ("__ompErr" in value && typeof value.__ompErr === "string") {
throw new ToolError(`${label} threw a JavaScript exception:\n${value.__ompErr}`);
}
if ("__ompPromise" in value && value.__ompPromise === true) {
throw new ToolError(
`${label} returned a Promise, but this surface evaluates synchronously and cannot await it — return a plain value (poll with waitForFunction for async state instead)`,
);
}
if ("__ompOk" in value) {
return value.__ompOk as TResult;
}
}
return value as TResult;
}
export function mapWaitUntil(waitUntil: string | undefined): "interactive" | "complete" {
return waitUntil === "domcontentloaded" ? "interactive" : "complete";
}
export interface ResolveCmuxKindOptions {
surface?: string;
settingEnabled?: boolean;
}View on GitHub (pinned to 9690622007)