koala73/worldmonitor · error

Scenario failed

Error message

Scenario failed

What it means

While polling a supply-chain scenario's status, a response with status 'failed' is treated as a terminal worker-side failure and surfaced as this generic Error. It means the backend/worker ran the scenario and reported failure, as opposed to timeout (no result in time) or a malformed done payload.

Solutions

  1. Retry the scenario — many worker failures are transient (restarts, upstream hiccups).
  2. Check the scenario worker's logs for the corresponding job to find the underlying failure.
  3. Simplify or adjust scenario parameters (smaller region/fewer layers) in case the specific input triggers the failure.
  4. Verify worker health/deployment status; if persistent, file an issue with the scenario id.

Example fix

// before
if (status.status === 'failed') throw new Error('Scenario failed');
// after
if (status.status === 'failed') {
  if (attempt < 2) { await sleep(backoff); continue poll; }
  throw new Error(`Scenario failed: ${status.error ?? 'unknown worker error'}`);
}
Defensive patterns

Strategy: retry

Type guard

function isScenarioStatus(s: unknown): s is { status: 'done' | 'failed' | string; result?: { topImpactCountries: unknown[] } } {
  return typeof s === 'object' && s !== null && 'status' in s;
}

Try / catch

try {
  const result = await runScenario(params);
} catch (e) {
  if (e.message === 'Scenario failed') {
    await retryWithBackoff(() => runScenario(params), { attempts: 2 });
  } else throw e;
}

Prevention

When it happens

Trigger: Polling loop receives a status object where status.status === 'failed' — the scenario worker processed the request but its computation errored or was marked failed server-side.

Common situations: Worker crashed mid-computation for this particular scenario; upstream data source for the scenario unavailable; scenario parameters that hit an unsupported code path; transient backend deploy/restart.

Related errors


AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15). Data as JSON: /api/errors/ff024e1926ac73c0. Report an issue: GitHub.

Appendix: source

Thrown at src/components/SupplyChainPanel.ts:1180

      const runResp = await runScenario({ scenarioId, iso2, disruptionPct }, { signal: runSignal });
      const jobId = runResp.jobId;
      let result: ScenarioResult | null = null;
      // 60 × 1s = 60s max (worker typically completes in <1s). 1s poll keeps
      // the perceived latency <2s in the common case. First iteration polls
      // immediately (no sleep) in case the worker was already running on a
      // previous job and blocked here only because of network round-trip.
      for (let i = 0; i < 60; i++) {
        if (signal.aborted) { resetButton('idle'); return; }
        if (!this.content.isConnected) return; // panel gone — nothing to update
        if (i > 0) await new Promise(r => setTimeout(r, 1000));
        const status = await getScenarioStatus(jobId, { signal });
        if (status.status === 'done') {
          const r = status.result;
          if (!r || !Array.isArray(r.topImpactCountries)) throw new Error('done without valid result');
          result = r;
          break;
        }
        if (status.status === 'failed') throw new Error('Scenario failed');
      }
      if (!result) throw new Error('Timeout — scenario worker may be down');
      if (signal.aborted) { resetButton('idle'); return; }
      if (!this.content.isConnected) return;
      // After this callback fires, showScenarioSummary() → render() will rebuild
      // the scenario-trigger DOM with the button already in its "Active" +
      // disabled state (driven by activeScenarioState in renderChokepoints()).
      // Do NOT touch the captured btn reference here — it's about to be detached
      // by render()'s setContent(), and any imperative update would no-op
      // silently while the fresh button shows the wrong state.
      this.scenarioRunState.delete(scenarioId);
      this.onScenarioActivate?.(scenarioId, result);
    } catch (err) {
      // Abort from a new click = user-triggered retry, no error banner needed.
      if (err instanceof Error && err.name === 'AbortError') {
        resetButton('idle');
        return;
      }

View on GitHub (pinned to 7d06c8633d)