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

  1. Await every async browser call inside the run body — the first listed rejection message identifies the failing call.
  2. If genuinely fire-and-forget, attach a .catch() that records the failure via the run's failure recorder.
  3. 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

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


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