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

  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.