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

  1. Normalize caller input: uppercase and trim before passing, e.g. computeScenario(id, raw?.trim().toUpperCase() ?? null, pct).
  2. Ensure 'no country selected' is encoded as null, not undefined, '' or 0.
  3. Map full names or alpha-3 codes to alpha-2 before the call (lookup table or lib such as i18n-iso-countries).
  4. 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

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


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)