can1357/oh-my-pi · error · ToolError
Unhandled rejection (missing await?): ${messages.join("\n[un
Error message
Unhandled rejection (missing await?): ${messages.join("\n[unhandled rejection] ")} What it means
The run runner collects promises that rejected without being awaited (`floatingRejections`) via facade wrappers. After the run body completes, if any remain, it throws this ToolError joining them with "[unhandled rejection]" prefixes. It surfaces fire-and-forget async failures that would otherwise be silently lost (the message hints the typical cause: missing await).
Source
Thrown at packages/coding-agent/src/tools/browser/cmux/cmux-tab.ts:1513
} catch (error) {
runFailed = true;
runError = error;
}
runAc.abort(runEndedError);
// Let rejection callbacks run while this run can still own guest-created promises.
await Bun.sleep(0);
if (hasFloatingFailure && !runFailed) await floatingFailure;
if (runFailed) {
for (const reason of activeRun.floatingRejections) {
logger.warn("Unhandled rejection accompanied a failed cmux browser run", { filename, error: reason });
}
throw runError;
}
if (activeRun.floatingRejections.length > 0) {
const messages = activeRun.floatingRejections.map(reason =>
reason instanceof Error ? reason.message : String(reason),
);
throw new ToolError(`Unhandled rejection (missing await?): ${messages.join("\n[unhandled rejection] ")}`, {
rejections: activeRun.floatingRejections,
});
}
return { displays: output.finish(), returnValue: cloneSafe(returnValue), screenshots };
} finally {
runActive = false;
uninstallRejectionInterceptor();
signal.removeEventListener("abort", onAbort);
runAc.abort(runEndedError);
activeCmuxRuns.delete(filename);
rememberCmuxRunFile(filename);
tab.clearRunContext();
}
}
function numberFrom(value: unknown, fallback: number): number {
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
}View on GitHub (pinned to 9690622007)
Solutions
- Await every async browser call inside the run body — the first listed rejection message identifies the failing call.
- If genuinely fire-and-forget, attach a .catch() that records the failure via the run's failure recorder.
- Use the run's signal/abort handling so abandoned work is cancelled rather than rejecting unobserved.
Example fix
// before
tab.click('#submit');
await tab.waitFor('.success');
// after
await tab.click('#submit'); // was rejecting unobserved
await tab.waitFor('.success'); Defensive patterns
Strategy: try-catch
Validate before calling
// lint/enforce: every promise-returning browser call in a run must be awaited
const results = await Promise.all([tab.click('#a'), tab.click('#b')]); // never bare calls Try / catch
try {
await runBody();
} catch (err) {
if (String(err.message).startsWith('Unhandled rejection (missing await?)')) {
const causes = String(err.message).split('[unhandled rejection] ').slice(1);
console.error('unawaited browser calls failed:', causes);
}
throw err;
} Prevention
- Enable lint rules (no-floating-promises) for run-body code.
- Await every async tab/page/browser call; if intentionally detached, attach .catch that records the failure.
- After refactors, grep run bodies for bare `tab.` / `page.` calls missing await.
When it happens
Trigger: Calling an async tab/page/browser method inside the run without `await` (navigation, evaluate, waitFor) whose promise later rejects; attaching .then without .catch; scheduling async work the run doesn't track.
Common situations: Refactors that dropped an await; intentionally fire-and-forget calls that fail (timeout, navigation error); Promise.all replaced by sequential statements where one call was left unawaited.
Related errors
- operation canceled
- aborted
- Unknown security operation: ${operationId}
- Background job manager unavailable for this session.
- Async bash execution is disabled. Enable async.enabled to us
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/591bd865338fdb65.
Report an issue: GitHub.