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 optimisticallyView on GitHub (pinned to b4c8548c09)
Solutions
- Re-query the current subscription list (e.g. currentUser { subscriptions } or the workspace equivalent) and only offer Cancel for plans that are actually active.
- Verify the identity matches the subscription: same userId (or workspaceId) and plan that getActiveSubscription searches for.
- 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
- Derive cancel/resume buttons from a fresh server subscription query, not cache.
- Send an Idempotency-Key header with cancel/resume calls.
- Treat subscription_not_exists as authoritative state, never retry it.
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
- cant_update_onetime_payment_subscription
- subscription_has_been_canceled
- workspace_id_required_to_update_team_subscription
- unsupported_subscription_plan
- managed_by_app_store_or_play
AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18).
Data as JSON: /api/errors/e7328dbd1d028207.
Report an issue: GitHub.