can1357/oh-my-pi · error · ToolError

${label} returned a Promise, but this surface evaluates sync

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.

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)

Solutions

  1. Remove async/await and return a plain synchronous value from the eval function (DOM reads like textContent/getBoundingClientRect are synchronous)
  2. If you need data from an async operation (fetch, storage), poll for it from the host with waitForFunction instead of awaiting inside eval
  3. 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

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


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/9d44a14f164d2bfd. Report an issue: GitHub.