koala73/worldmonitor · error · Error

Unknown scenario: ${scenarioId}

Error message

Unknown scenario: ${scenarioId}

What it means

computeScenario(scenarioId, iso2, disruptionPct) in scripts/scenario-worker.mjs resolves the scenario by looking up scenarioId in the SCENARIO_TEMPLATES list. If no template has a matching id, it throws `Unknown scenario: ${scenarioId}` because only template-defined scenarios can be computed.

Solutions

  1. Log/inspect SCENARIO_TEMPLATES and use an exact existing id (they are case-sensitive)
  2. Validate the scenario id against the template list before calling computeScenario (or return a 400 to the caller)
  3. Check for renames: if an id changed in SCENARIO_TEMPLATES, update clients/caches still sending the old id

Example fix

// before
await computeScenario('tariff shock', 'CN');
// after
const id = 'tariff-shock';
if (!SCENARIO_TEMPLATES.some(t => t.id === id)) throw new Error(`Unsupported scenario: ${id}`);
await computeScenario(id, 'CN');
Defensive patterns

Strategy: validation

Validate before calling

export const SCENARIO_IDS = new Set(SCENARIO_TEMPLATES.map(t => t.id));
function assertValidScenarioId(id) {
  if (typeof id !== 'string' || !SCENARIO_IDS.has(id)) {
    throw new Error(`Unknown scenario: ${id}; valid ids: ${[...SCENARIO_IDS].join(', ')}`);
  }
}

Type guard

const isScenarioId = (id) =>
  typeof id === 'string' && SCENARIO_TEMPLATES.some(t => t.id === id);

Try / catch

try {
  const result = await computeScenario(scenarioId, iso2, disruptionPct);
  return result;
} catch (err) {
  if (/^Unknown scenario: /.test(err.message)) {
    return { error: 'unknown_scenario', scenarioId, valid: SCENARIO_TEMPLATES.map(t => t.id) };
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling computeScenario('tariff-shok', ...) or any id not present in SCENARIO_TEMPLATES — via direct import, a worker message, or an API/CLI path that forwards a raw scenario id from client input.

Common situations: Typo or outdated scenario id sent from a UI/form; ids renamed in SCENARIO_TEMPLATES while cached clients still send the old id; case-sensitivity mismatch ('Tariff-Shock' vs 'tariff-shock'); building an id dynamically by string concatenation.

Understand the failure class

Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.

Related errors


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

Appendix: source

Thrown at scripts/scenario-worker.mjs:230

}

// ────────────────────────────────────────────────────────────────────────────
// Scenario computation
// ────────────────────────────────────────────────────────────────────────────

/** @param {number} score @param {number} severity @param {number} multiplier */
export function physicalImpact(score, severity, multiplier) {
  return score * (severity / 100) * multiplier;
}

/**
 * @param {string} scenarioId
 * @param {string | null} iso2
 * @param {number | undefined} [disruptionPct]
 */
export async function computeScenario(scenarioId, iso2, disruptionPct) {
  const template = SCENARIO_TEMPLATES.find(t => t.id === scenarioId);
  if (!template) throw new Error(`Unknown scenario: ${scenarioId}`);
  const isTariffShock = template.affectedChokepointIds.length === 0;
  if (iso2 !== null && (typeof iso2 !== 'string' || !/^[A-Z]{2}$/.test(iso2))) {
    throw new Error('Invalid country');
  }
  if (disruptionPct !== undefined && (isTariffShock || !Number.isInteger(disruptionPct) || disruptionPct < 0 || disruptionPct > 100)) {
    throw new Error('Invalid disruption override');
  }
  const severity = disruptionPct ?? template.disruptionPct;
  const manifest = await redisGet('seed-meta:supply_chain:chokepoint-exposure').catch(() => null);
  const validIds = (values, pattern, limit) => Array.isArray(values) && values.length > 0
    && values.length <= limit && values.every(v => typeof v === 'string' && pattern.test(v))
    && new Set(values).size === values.length;
  // Deliberately NOT gated on `manifest.status === 'ok'`. The manifest's country/sector
  // arrays describe the seeder's static universe, not the outcome of its last run — a
  // failed run leaves them true while invalidating per-key freshness, which the per-record
  // `missing` state already reports. Requiring 'ok' here turned any single seeder failure
  // into a total feature blackout even though the exposure keys stay TTL-extended.
  const manifestKnown = manifest?.manifestVersion === 1

View on GitHub (pinned to 7d06c8633d)