koala73/worldmonitor · error · Error
Dashboard is no longer available.
Error message
Dashboard is no longer available.
What it means
Plain Error (not DashboardBindingError) thrown by waitForWebMcpUiReady() when the appDestroyed promise wins the three-way race against uiReady and the timeout timer. The caller awaited UI initialization, but the app was destroyed during that wait — e.g. startup was torn down before Phase-4 UI init completed. Note the sibling outcomes: the timeout arm throws a different message ('... did not initialise within ...ms'), and applyWebMcpDashboardAction does not throw at all for a destroyed app — it returns APP_DESTROYED_RESULT.
Source
Thrown at src/app/webmcp-dashboard.ts:89
appDestroyed: Promise<void>,
timeoutMs: number,
target = 'UI',
): Promise<void> {
let timer: ReturnType<typeof setTimeout> | null = null;
const timeout = new Promise<never>((_, reject) => {
timer = setTimeout(
() => reject(new Error(`${target} did not initialise within ${timeoutMs}ms`)),
timeoutMs,
);
});
try {
const outcome = await Promise.race([
uiReady.then(() => 'ready' as const),
appDestroyed.then(() => 'destroyed' as const),
timeout,
]);
if (outcome === 'destroyed') {
throw new Error('Dashboard is no longer available.');
}
} finally {
if (timer !== null) clearTimeout(timer);
}
}
export async function applyWebMcpDashboardAction(
ctx: AppContext,
action: unknown,
options: AgentBusApplierOptions,
): Promise<DashboardActionResult> {
if (ctx.isDestroyed) return APP_DESTROYED_RESULT;
// Keep the zod-backed agent-bus contract out of the eager dashboard entry.
const { applyAgentBusAction } = await import('./agent-bus-applier');
if (ctx.isDestroyed) return APP_DESTROYED_RESULT;
const result = applyAgentBusAction(ctx, action, options);
if (result.ok && result.actionType === 'set_view' && ctx.map) {View on GitHub (pinned to eeab0a219f)
Solutions
- Catch this error in the tool binding and return a terminal 'app destroyed' result to the agent instead of letting it escape as a 500-style tool error
- Delay tool registration or queue early tool calls until uiReady settles, so destroy-during-startup is handled by one place
- In tests, await the init promise before destroy() to eliminate the race
Example fix
// before
await waitForWebMcpUiReady(this.uiReady, this.appDestroyed, 10_000); // throws raw Error
// after
try {
await waitForWebMcpUiReady(this.uiReady, this.appDestroyed, 10_000);
} catch (error) {
return { ok: false, status: 'denied', reason: 'app_destroyed' } as const;
} Defensive patterns
Strategy: try-catch
Validate before calling
if (appDestroyed.settled) return DESTROYED_RESULT; // if you track the promise, check before awaiting await Promise.race([uiReady, appDestroyed, timeout]);
Try / catch
try { await waitForWebMcpUiReady(uiReady, appDestroyed, 10_000); } catch (e) { if (e instanceof Error && e.message === 'Dashboard is no longer available.') return DESTROYED_RESULT; if (e instanceof Error && e.message.includes('did not initialise within')) return TIMEOUT_RESULT; throw e; } Prevention
- Distinguish this destroy-during-startup error from the sibling timeout message before choosing a fix
- Queue early tool calls until init settles
- In e2e tests, await init before destroy
When it happens
Trigger: A WebMCP tool invoked during the startup window while the app is destroyed before uiReady resolves: user closes the tab during boot, HMR replaces the app mid-init, or tests destroy() the app while first paint is still pending. The awaited Promise.race resolves 'destroyed', which converts to this throw.
Common situations: Agent clients probing tools immediately on page load while the user navigates away; flaky e2e teardown that destroys the app before init finishes; double-mount/unmount cycles in dev frameworks.
Related errors
- app_destroyed
- Search manager destroyed
- app_destroyed
- Country brief panel is not initialised
- map_unavailable
AI-assisted analysis of koala73/worldmonitor@eeab0a219f (2026-08-21).
Data as JSON: /api/errors/247b9eb8d32ecc57.
Report an issue: GitHub.