koala73/worldmonitor · warning · ConvexError

ACTIVE_SUBSCRIPTION_EXISTS

ACTIVE_SUBSCRIPTION_EXISTS

Error message

A ${subscription.displayName} subscription already exists for this account. Use Manage Billing to update it instead of purchasing again.

What it means

`createCheckout` runs `getCheckoutBlockingSubscription` first; if the user already has an active subscription that blocks this product, checkout is refused and the error carries `buildBlockedCheckoutPayload(blocking)` with `ACTIVE_SUBSCRIPTION_EXISTS`. The message directs the user to Manage Billing rather than re-purchasing. This guard always runs, even with `bypassPendingGuard`.

Source

Thrown at convex/payments/checkout.ts:368

    const identity = await resolveUserIdentity(ctx);
    if (args.bypassPendingGuard) {
      // Audit trail: the user confirmed "start a new checkout anyway" past a
      // pending-payment block. Logged server-side so a future double-charge
      // investigation has the bypass record (#4438 review — the original
      // incident was undetected stacked payments).
      console.info(`[checkout] pending-payment guard bypassed user=${userId} product=${args.productId}`);
    }
    // Run both guards concurrently — they share no data, so serial awaits only
    // add a Convex round-trip to every checkout (#4438 review). Subscription
    // block still WINS (evaluated first); bypass skips the pending query.
    const [blocking, pending] = await Promise.all([
      getCheckoutBlockingSubscription(ctx, userId, args.productId),
      args.bypassPendingGuard
        ? Promise.resolve(null)
        : getCheckoutBlockingPendingPayment(ctx, userId, args.productId),
    ]);
    if (blocking) {
      throw new ConvexError(buildBlockedCheckoutPayload(blocking));
    }
    if (pending) {
      throw new ConvexError(buildPendingBlockedPayload(pending));
    }

    const customerName = identity
      ? [identity.givenName, identity.familyName].filter(Boolean).join(" ") ||
        identity.name
      : undefined;

    const result = await _createCheckoutSession(args, {
      userId,
      email: identity?.email,
      name: customerName,
    });
    // The public Convex action historically rejects provider failures. Keep
    // that error-channel contract: only the trusted internal relay consumes
    // the typed outcome and translates it into HTTP 429 + Retry-After.

View on GitHub (pinned to ffec79ac33)

Solutions

  1. Use Manage Billing to update/cancel the existing subscription instead of purchasing again
  2. Cancel or let the existing subscription lapse before re-purchasing
  3. Reflect the existing subscription in the purchase UI to prevent the attempt
Defensive patterns

Strategy: validation

Validate before calling

// Reflect existing subscription state before offering purchase.
const sub = await convex.query(api.payments.subscriptions.activeForProduct, { productId: args.productId });
if (sub) { /* route to Manage Billing instead of createCheckout */ }

Try / catch

try {
  await convex.action(api.payments.checkout.createCheckout, args);
} catch (err) {
  if (err.data?.code === 'ACTIVE_SUBSCRIPTION_EXISTS') {
    // redirect to Manage Billing
  } else { throw err; }
}

Prevention

When it happens

Trigger: Calling `createCheckout` for a product the user already actively subscribes to; attempting to stack an identical subscription.

Common situations: User forgot they are already subscribed; double-purchase attempt; UI failed to reflect existing subscription state.

Related errors


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