koala73/worldmonitor · error · Error
Invalid country
Error message
Invalid country
What it means
computeScenario() validates the iso2 country parameter before running a supply-chain scenario. The parameter must be either exactly null (meaning 'global, all countries') or a 2-letter uppercase ISO-3166 alpha-2 code. Any other value — lowercase, 3 letters, a full country name, a number, or a non-string — throws this error rather than silently producing wrong scenario output.
Solutions
- Normalize caller input: uppercase and trim before passing, e.g. computeScenario(id, raw?.trim().toUpperCase() ?? null, pct).
- Ensure 'no country selected' is encoded as null, not undefined, '' or 0.
- Map full names or alpha-3 codes to alpha-2 before the call (lookup table or lib such as i18n-iso-countries).
- Add a pre-call validation with the same regex so the failure surfaces at the boundary with context.
Example fix
// before
await computeScenario('chokepoint-closure', 'usa', 30);
// after
const iso2 = rawCountry ? rawCountry.trim().toUpperCase() : null; // 'usa' -> 'USA', absent -> null
if (iso2 !== null && !/^[A-Z]{2}$/.test(iso2)) throw new Error(`Bad country code: ${rawCountry}`);
await computeScenario('chokepoint-closure', iso2, 30); Defensive patterns
Strategy: validation
Validate before calling
function toIso2(raw) {
if (raw === null || raw === undefined || raw === '') return null;
const v = String(raw).trim().toUpperCase();
if (!/^[A-Z]{2}$/.test(v)) throw new Error(`Not an ISO-3166 alpha-2 code: ${raw}`);
return v;
}
const iso2 = toIso2(userCountry);
await computeScenario(scenarioId, iso2, pct); Type guard
const isIso2 = (v) => v === null || (typeof v === 'string' && /^[A-Z]{2}$/.test(v)); Try / catch
try {
await computeScenario(id, iso2, pct);
} catch (e) {
if (e.message === 'Invalid country') {
notifyUser(`'${iso2}' is not a valid 2-letter country code`);
return;
}
throw e;
} Prevention
- Normalize with trim().toUpperCase() at the input boundary, once.
- Represent 'no country' as null everywhere, never undefined.
- Keep an alpha-2 whitelist sourced from your chokepoint-exposure data.
- Unit-test the validator against lowercase, alpha-3, and empty inputs.
When it happens
Trigger: Calling computeScenario(scenarioId, iso2, disruptionPct) where iso2 is not null and fails /^[A-Z]{2}$/: e.g. 'usa', 'us', 'USA', '', 'US ', 840, undefined (undefined !== null), or a full name like 'United States'.
Common situations: Passing a user-supplied country picker value that is a country name or numeric code; forgetting to convert lowercase input with .toUpperCase(); defaulting the parameter to undefined instead of null; UI sending 3-letter alpha-3 codes.
Understand the failure class
Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.
Related errors
- Could not resolve ${JSON.stringify(echoCountryInput(raw))} t
- Invalid disruption override
- Could not resolve ${JSON.stringify(echoCountryInput(raw))} t
- digestHour must be an integer 0–23
- quietHoursStart must be an integer 0–23
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/07f0d2fda5c6d203.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/scenario-worker.mjs:233
// 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
&& validIds(manifest.countryIds, /^[A-Z]{2}$/, 250)
&& validIds(manifest.hs2Codes, /^(0[1-9]|[1-9][0-9])$/, 99);
const countryIds = iso2 ? [iso2] : manifestKnown ? manifest.countryIds : [];View on GitHub (pinned to 7d06c8633d)