toeverything/AFFiNE · error · SubscriptionNotExists

subscription_not_exists

subscription_not_exists

Error message

You didn't subscribe to the ${plan} plan.

What it means

Thrown by SubscriptionService.cancelSubscription when manager.getActiveSubscription(identity) returns null, i.e. there is no active subscription for the given identity (user or workspace) and plan. Cancel is only valid against an existing active subscription.

Source

Thrown at packages/backend/server/src/plugins/payment/service.ts:168

        recurring,
        variant: variant ?? null,
      },
      params,
      args
    );
  }

  async cancelSubscription(
    identity: z.infer<typeof SubscriptionIdentity>,
    idempotencyKey?: string
  ): Promise<Subscription> {
    this.assertSubscriptionIdentity(identity);

    const manager = this.select(identity.plan);
    const subscription = await manager.getActiveSubscription(identity);

    if (!subscription) {
      throw new SubscriptionNotExists({ plan: identity.plan });
    }

    // IAP read-only: RevenueCat-managed subscriptions cannot be modified on web
    if (subscription.provider === 'revenuecat') {
      throw new ManagedByAppStoreOrPlay();
    }

    if (!subscription.stripeSubscriptionId) {
      throw new CantUpdateOnetimePaymentSubscription(
        'Onetime payment subscription cannot be canceled.'
      );
    }

    if (subscription.canceledAt) {
      throw new SubscriptionHasBeenCanceled();
    }

    // update the subscription in db optimistically

View on GitHub (pinned to b4c8548c09)

Solutions

  1. Re-query the current subscription list (e.g. currentUser { subscriptions } or the workspace equivalent) and only offer Cancel for plans that are actually active.
  2. Verify the identity matches the subscription: same userId (or workspaceId) and plan that getActiveSubscription searches for.
  3. Treat this code in async flows as 'already gone': skip retry and refresh local state from the server.

Example fix

// before
setBusy(true);
await svc.cancelSubscription({ userId, plan });

// after
const subs = await listSubscriptions();
const active = subs.find((s) => s.plan === plan && s.status === 'active');
if (!active) return refresh();
await svc.cancelSubscription({ userId, plan });
Defensive patterns

Strategy: validation

Validate before calling

const subs = await listSubscriptions();
const active = subs.some((s) => s.plan === identity.plan && s.status === 'active');
if (!active) throw new Error('No active subscription to cancel');
await svc.cancelSubscription(identity, idempotencyKey);

Type guard

interface SubLike { plan: string; status: string; }
function hasActivePlan(subs: SubLike[], plan: string): boolean {
  return subs.some((s) => s.plan === plan && s.status === 'active');
}

Try / catch

catch (e) { if (gqlErrorCode(e) === 'subscription_not_exists') { await refreshSubscriptions(); notify('You are not subscribed to this plan.'); return; } throw e; }

Prevention

When it happens

Trigger: Calling cancelSubscription({ userId, plan: 'pro' }) when the user is on Free or already fully expired; canceling a team plan with a workspaceId that has no subscription; plan/recurring mismatch with what was actually purchased.

Common situations: Stale UI showing a Cancel button after the subscription lapsed or was removed by webhook; user canceled earlier and a sync race re-showed active state; wrong workspace selected in a multi-workspace client.

Related errors


AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18). Data as JSON: /api/errors/e7328dbd1d028207. Report an issue: GitHub.