koala73/worldmonitor · error · ConvexError

NO_EMAILS_PROVIDED

NO_EMAILS_PROVIDED

Error message

NO_EMAILS_PROVIDED

What it means

Thrown by `inviteSeats` when, after trimming, lowercasing, deduplicating, and filtering empties, the `emails` array is empty. The handler normalizes the input first, so this fires only when every supplied email was blank. Object-typed ConvexError (`{ kind: "NO_EMAILS_PROVIDED" }`).

Source

Thrown at convex/payments/businessSeats.ts:154

    if (!ownerDomain || !isCorporateDomain(ownerEmail)) {
      throw new ConvexError({ kind: "OWNER_DOMAIN_NOT_CORPORATE" });
    }

    // Serialize concurrent inviteSeats / removeSeat calls for this Business
    // subscription so the cap check below cannot be bypassed by a race.
    await touchBusinessSeatLock(ctx, businessSub.dodoSubscriptionId, now);

    const normalizedOwnerEmail = ownerEmail.toLowerCase();
    // Dedupe: the existingByEmail check below only guards against emails
    // that already had a grant BEFORE this call — a duplicate within the
    // SAME args.emails array would otherwise slip past it (neither
    // occurrence is in existingGrants yet) and create two grant rows for
    // one invitee.
    const emails = Array.from(
      new Set(args.emails.map((e) => e.trim().toLowerCase()).filter((e) => e.length > 0)),
    );
    if (emails.length === 0) {
      throw new ConvexError({ kind: "NO_EMAILS_PROVIDED" });
    }
    if (emails.length > MAX_SEATS) {
      throw new ConvexError({ kind: "TOO_MANY_EMAILS" });
    }

    // Check existing grants for duplicates (active or pending).
    const existingGrants = await ctx.db
      .query("businessProGrants")
      .withIndex("by_businessSubscriptionId", (q) =>
        q.eq("businessSubscriptionId", businessSub.dodoSubscriptionId),
      )
      .collect();
    const existingByEmail = new Map(
      existingGrants.map((g) => [g.inviteeEmail, g]),
    );

    // Separate new invites from duplicates before the cap check so a duplicate
    // re-invite is idempotent even when the cap is full.

View on GitHub (pinned to ffec79ac33)

Solutions

  1. Validate the emails array is non-empty before submitting the mutation.
  2. Disable the invite button until at least one non-blank email is entered.
  3. Filter blank entries client-side and require `length >= 1`.

Example fix

// before
await inviteSeats({ emails: rawEmails }); // rawEmails = ["", "  "]

// after
const emails = rawEmails.map(e => e.trim()).filter(Boolean);
if (emails.length === 0) return;
await inviteSeats({ emails });
Defensive patterns

Strategy: validation

Validate before calling

const emails = rawEmails.map(e => e.trim().toLowerCase()).filter(Boolean);
if (emails.length === 0) {
  showToast("Add at least one email.");
  return;
}
await inviteSeats({ emails });

Type guard

function hasEmails(arr: string[]): boolean {
  return arr.map(e => e.trim()).filter(e => e.length > 0).length > 0;
}

Prevention

When it happens

Trigger: Calling `inviteSeats({ emails: [] })`, or with an array of only whitespace/empty strings (e.g. `["", " "]`).

Common situations: The invite form submitted with no recipients entered; a CSV paste of blank lines; the UI forwarded an unvalidated empty list.

Related errors


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