hcengineering/platform · error · Error
Failed to cancel subscription ${providerSubscriptionId}
Error message
Failed to cancel subscription ${providerSubscriptionId} What it means
After cancelSubscription successfully calls Stripe to cancel the subscription, the raw Stripe.Subscription is converted via transformStripeSubscriptionToData. If that transform returns null (the subscription is in a state the mapper does not recognize / cannot represent), cancelSubscription throws this error even though the Stripe-side cancellation already happened. It signals 'cancel succeeded at Stripe but result could not be mapped to SubscriptionData'.
Source
Thrown at services/payment/pod-payment/src/providers/stripe/provider.ts:277
ctx.info('Stripe subscription reconciliation completed', {
stripeActiveCount: stripeActiveSubscriptions.length,
ourActiveCount: ourActiveSubscriptions.length,
upsertedCount: upsertCount,
staleUpdatedCount: staleCount
})
} catch (err) {
ctx.error('Stripe subscription reconciliation failed', { err })
throw err
}
}
async cancelSubscription (ctx: MeasureContext, providerSubscriptionId: string): Promise<SubscriptionData> {
const stripeSubscription = await this.stripe.cancelSubscription(ctx, providerSubscriptionId)
const subscriptionData = transformStripeSubscriptionToData(ctx, stripeSubscription)
if (subscriptionData == null) {
throw new Error(`Failed to cancel subscription ${providerSubscriptionId}`)
}
return subscriptionData
}
async uncancelSubscription (ctx: MeasureContext, providerSubscriptionId: string): Promise<SubscriptionData> {
const stripeSubscription = await this.stripe.uncancelSubscription(ctx, providerSubscriptionId)
const subscriptionData = transformStripeSubscriptionToData(ctx, stripeSubscription)
if (subscriptionData == null) {
throw new Error(`Failed to uncancel subscription ${providerSubscriptionId}`)
}
return subscriptionData
}
async updateSubscriptionPlan (
ctx: MeasureContext,
subscriptionId: string,View on GitHub (pinned to 63e28dc964)
Solutions
- Verify the providerSubscriptionId points to an active Stripe subscription before cancelling
- Check transformStripeSubscriptionToData in providers/stripe/utils.ts and extend it to handle the status that returns null (e.g. 'canceled')
- Check in Stripe dashboard/logs what status the subscription actually had at cancel time
- Treat the operation idempotently: if the subscription is already canceled, fetch and return its data instead of throwing
Example fix
// before
const subscriptionData = transformStripeSubscriptionToData(ctx, stripeSubscription)
if (subscriptionData == null) {
throw new Error(`Failed to cancel subscription ${providerSubscriptionId}`)
}
// after
const subscriptionData =
transformStripeSubscriptionToData(ctx, stripeSubscription) ?? {
providerSubscriptionId,
status: 'canceled' // fallback for already-canceled subs the mapper skips
} as unknown as SubscriptionData Defensive patterns
Strategy: try-catch
Validate before calling
// check current subscription state before cancelling
const sub = await paymentProvider.getSubscription(ctx, providerSubscriptionId)
if (sub == null || sub.status === 'canceled') {
return sub // already canceled; nothing to do
} Type guard
function isSubscriptionData(x: SubscriptionData | null | undefined): x is SubscriptionData {
return x != null && typeof x.providerSubscriptionId === 'string'
} Try / catch
try {
return await paymentProvider.cancelSubscription(ctx, providerSubscriptionId)
} catch (err) {
if ((err as Error).message.startsWith('Failed to cancel subscription')) {
// Stripe-side cancel may already have happened; verify actual state before retrying
const current = await paymentProvider.getSubscription(ctx, providerSubscriptionId)
if (current?.status === 'canceled') return current
}
throw err
} Prevention
- Make cancel operations idempotent in callers (check status first, tolerate already-canceled)
- Keep transformStripeSubscriptionToData covering every Stripe subscription status you can encounter
- Avoid double-click/duplicate cancel requests from the UI (disable button while in flight)
- Watch Stripe dashboard for subscriptions canceled outside the app
When it happens
Trigger: Calling cancelSubscription with a providerSubscriptionId whose Stripe subscription is in an unexpected status (e.g. already canceled/incomplete_expired) such that transformStripeSubscriptionToData returns null; a subscription object missing fields the mapper requires.
Common situations: Cancelling the same subscription twice (second call hits an already-canceled subscription the mapper skips); Stripe-side changes via dashboard producing statuses the transform doesn't handle; stale subscription IDs after test-mode/live-mode switching.
Related errors
- Failed to uncancel subscription ${providerSubscriptionId}
- Missing priceId for plan: ${planKey}
- No price configured for plan: ${planKey}
- BillingError: server response text
- IntegrationAlreadyExists
AI-assisted analysis of hcengineering/platform@63e28dc964 (2026-08-29).
Data as JSON: /api/errors/87540f4e3f66ebb6.
Report an issue: GitHub.