koala73/worldmonitor · error · ConvexError
INVALID_COUNTRY
INVALID_COUNTRY
Error message
INVALID_COUNTRY
What it means
Thrown by `followCountry` after the auth gate passes but `isValidIso2(args.country)` returns false. The mutation takes a single `country` string and requires it to be a valid ISO 3166-1 alpha-2 code (e.g. "US", "JP", "DE"). Carries object data `{ kind: "INVALID_COUNTRY", country: args.country }` so the client can echo back the offending value. This throw is intentionally NOT a return-value (unlike the FREE_CAP branch) — invalid input is a client bug and is wanted in Sentry.
Source
Thrown at convex/followedCountries.ts:347
*
* Without Tier 1, two parallel first-ever mutations could both read
* `meta=undefined`, both INSERT, and produce duplicate meta rows that
* break the `.unique()` read AND re-open the cap-bypass window. With
* Tier 1 in place, the brand-new-user race is closed deterministically.
*
* Errors are typed `ConvexError({kind, ...})` with object data so callers
* can branch on `err.data.kind` (memory:
* `convex-error-string-data-strips-errordata-on-wire`).
*/
export const followCountry = mutation({
args: { country: v.string() },
handler: async (ctx, args): Promise<FollowMutationResult> => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new ConvexError({ kind: "UNAUTHENTICATED" });
const userId = identity.subject;
if (!isValidIso2(args.country)) {
throw new ConvexError({
kind: "INVALID_COUNTRY",
country: args.country,
});
}
// Tier-1 lock: pre-seeded shard row. Read at top, patch at end.
const shard = await readShardOrThrow(ctx, userId);
// Tier-2 read: per-user denormalized count (lazy-created, but safe
// under the shard lock above).
const { meta, count: currentCount } = await readUserMeta(ctx, userId);
const existingRow = await ctx.db
.query("followedCountries")
.withIndex("by_user_country", (q) =>
q.eq("userId", userId).eq("country", args.country),
)
.first();View on GitHub (pinned to ffec79ac33)
Solutions
- Pass only validated ISO 3166-1 alpha-2 codes (uppercase, 2 letters) from a static registry, not from user input or display names.
- Client-side: gate the follow button on `isValidIso2(country)` using the same registry the server uses.
- On `err.data.kind === "INVALID_COUNTRY"`, log `err.data.country` to find which source emits bad codes.
- Normalize upstream ISO3 -> ISO2 before calling (e.g. "USA" -> "US").
Example fix
// before
await convex.mutation(api.followedCountries.followCountry, { country: countryName });
// after — send the validated ISO2 code
const ISO2 = new Set(["US","JP","DE","GB","FR",/* ... full registry */]);
const code = toIso2(countryName); // your normalizer
if (!ISO2.has(code)) { console.warn("bad country", countryName); return; }
await convex.mutation(api.followedCountries.followCountry, { country: code }); Defensive patterns
Strategy: validation
Validate before calling
const ISO2 = new Set(["US","JP","DE","GB","FR","CA","AU",/* ... full ISO 3166-1 alpha-2 registry */]);
function toValidIso2(code: string): string | null {
const c = code.trim().toUpperCase();
return ISO2.has(c) ? c : null;
}
const code = toValidIso2(input);
if (!code) { console.warn("invalid country", input); return; } Type guard
function isValidIso2(code: string): boolean {
return /^[A-Z]{2}$/.test(code) && ISO2_REGISTRY.has(code);
} Try / catch
try {
await convex.mutation(api.followedCountries.followCountry, { country });
} catch (err) {
if (err.data?.kind === "INVALID_COUNTRY") console.warn("bad code", err.data.country);
else throw err;
} Prevention
- Maintain a single ISO2 registry shared by client and server.
- Never pass display names or ISO3 codes to country mutations.
- Normalize upstream ISO3/numeric/name to ISO2 before calling.
- Log err.data.country to find the emitting source.
When it happens
Trigger: Calling `followCountry` with a non-ISO2 string: a full country name ("United States"), an ISO3 code ("USA"), lowercase ("us"), an empty string, a numeric code, or a typo ("UX"). Also a stale panel config referencing a retired/renamed code.
Common situations: Frontend passes the country display label instead of the code; an upstream data source emits ISO3 and the conversion was skipped; a user-follow action built from a free-text field; a code list that drifted from the ISO registry (e.g. "XK" for Kosovo, which is not in ISO 3166-1).
Related errors
- INCOMPATIBLE_DELIVERY
- COUNTRIES_LIMIT_EXCEEDED
- TICKERS_LIMIT_EXCEEDED
- digestHour must be an integer 0–23
- digestTimezone must be a valid IANA timezone (e.g. America/N
AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12).
Data as JSON: /api/errors/7919d31698302884.
Report an issue: GitHub.