hcengineering/platform · error · Error

Failed to cancel subscription

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'.

Solutions

  1. Verify the providerSubscriptionId points to an active Stripe subscription before cancelling
  2. Check transformStripeSubscriptionToData in providers/stripe/utils.ts and extend it to handle the status that returns null (e.g. 'canceled')
  3. Check in Stripe dashboard/logs what status the subscription actually had at cancel time
  4. 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

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


AI-assisted analysis of hcengineering/platform@63e28dc964 (2026-08-29). Data as JSON: /api/errors/87540f4e3f66ebb6. Report an issue: GitHub.

Appendix: 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)