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
- Log/inspect SCENARIO_TEMPLATES and use an exact existing id (they are case-sensitive)
- Validate the scenario id against the template list before calling computeScenario (or return a 400 to the caller)
- 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
- Expose the list of valid scenario ids to clients (e.g. via an endpoint or shared constant) instead of free-form strings
- Validate ids at the boundary (API/worker message) before reaching computeScenario
- Treat scenario ids as an enum: rename centrally and update all callers in one change
- Watch for case/spacing differences in user-supplied ids; normalize (lowercase, trim) before lookup only if the template ids are lowercase
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
- malformed_arguments
- invalid_category
- disruptionPct must be an integer from 0 to 100 for a physica
- ${label} HTTP 400
- Could not resolve ${JSON.stringify(echoCountryInput(raw))} t
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 === 1View on GitHub (pinned to 7d06c8633d)