koala73/worldmonitor · warning · ConvexError

EMPTY_INPUT

EMPTY_INPUT

Error message

EMPTY_INPUT

What it means

Thrown by `mergeAnonymousLocal` (Step 2) when `args.countries.length === 0`. This is a guard before the upper-bound and ISO2-filter steps. Merging zero countries is a no-op and indicates the client dispatched the merge with no local state. Carries `{ kind: "EMPTY_INPUT" }`. It is distinct from `INPUT_TOO_LARGE` (too many) and from a successful merge that drops all-invalid codes (which returns `droppedInvalid` instead of throwing).

Source

Thrown at convex/followedCountries.ts:497

 *
 * Resolves Codex-deepening round-1 P0 (server-side cap on merge) and
 * round-2 P1 (canonicalize duplicates before counting). Free users with
 * existingCount >= LIMIT accept zero new rows — never silently grow above
 * the cap during merge. (Grandfathering above-cap rows on PRO→free
 * downgrade is a separate concern handled by NOT auto-deleting on
 * downgrade; merge is the FIRST sign-in and has no PRO history to
 * grandfather.)
 */
export const mergeAnonymousLocal = mutation({
  args: { countries: v.array(v.string()) },
  handler: async (ctx, args): Promise<MergeAnonymousLocalResult> => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new ConvexError({ kind: "UNAUTHENTICATED" });
    const userId = identity.subject;

    // Step 2: empty-input guard.
    if (args.countries.length === 0) {
      throw new ConvexError({ kind: "EMPTY_INPUT" });
    }

    // Step 3: defensive upper-bound on input length.
    if (args.countries.length > MAX_MERGE_INPUT) {
      throw new ConvexError({
        kind: "INPUT_TOO_LARGE",
        max: MAX_MERGE_INPUT,
        received: args.countries.length,
      });
    }

    // Step 4: ISO-2 registry filter; collect droppedInvalid in input order.
    const droppedInvalid: string[] = [];
    const validInputs: string[] = [];
    for (const code of args.countries) {
      if (isValidIso2(code)) {
        validInputs.push(code);
      } else {

View on GitHub (pinned to ffec79ac33)

Solutions

  1. Client-side: only call `mergeAnonymousLocal` when `localCountries.length > 0`.
  2. On `err.data.kind === "EMPTY_INPUT"`, treat as a no-op success (clear any pending-merge flag) — there is nothing to merge.
  3. Initialize local follows from localStorage with a default of `[]` and skip the mutation when empty.

Example fix

// before
await convex.mutation(api.followedCountries.mergeAnonymousLocal, { countries: localFollows ?? [] });

// after
const local = JSON.parse(localStorage.getItem("anonFollows") ?? "[]");
if (local.length === 0) { localStorage.removeItem("pendingMerge"); return; }
await convex.mutation(api.followedCountries.mergeAnonymousLocal, { countries: local });
Defensive patterns

Strategy: validation

Validate before calling

const local: string[] = JSON.parse(localStorage.getItem("anonFollows") ?? "[]");
if (!Array.isArray(local) || local.length === 0) {
  localStorage.removeItem("pendingMerge");
  return; // nothing to merge
}

Type guard

function hasMergeInput(countries: unknown): countries is string[] {
  return Array.isArray(countries) && countries.length > 0;
}

Try / catch

try {
  await convex.mutation(api.followedCountries.mergeAnonymousLocal, { countries: local });
} catch (err) {
  if (err.data?.kind === "EMPTY_INPUT") { /* no-op success */ localStorage.removeItem("pendingMerge"); }
  else throw err;
}

Prevention

When it happens

Trigger: Calling `mergeAnonymousLocal` with `countries: []`; the local-storage key for anonymous follows is missing/empty and the client unconditionally dispatches the merge; a first sign-in for a user who never followed anything anonymously.

Common situations: The anonymous-follows localStorage is cleared (different browser, incognito) and the merge fires anyway; the merge is wired to a sign-in event without checking whether local follows exist; a migration path that always calls merge "just in case".

Related errors


AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12). Data as JSON: /api/errors/ff1f42b9493b25d4. Report an issue: GitHub.