TryGhost/Ghost · error · Error

Expected redeemed offer member ${emailAddress} to have a sub

Error message

Expected redeemed offer member ${emailAddress} to have a subscription

What it means

Thrown in `redeemOfferViaPortal` after fetching the member via `membersService.getByEmailWithSubscriptions(emailAddress)`. It asserts `member.subscriptions[0]` exists — the offer redemption is expected to create an active subscription. A missing subscription means the checkout completed in the UI but Ghost's backend never recorded a subscription.

Source

Thrown at e2e/helpers/playwright/flows/offers.ts:98

    name: string;
    subscription: MemberSubscription;
}> {
    const membersService = new MembersService(page.request);
    const {emailAddress, name} = await completeOfferSignupViaPortal(page, stripe, opts);

    const homePage = new HomePage(page);
    await homePage.goto();
    await homePage.openAccountPortal();

    const accountPage = new PortalAccountPage(page);
    await accountPage.waitForPortalToOpen();
    await accountPage.emailText(emailAddress).waitFor({state: 'visible'});

    const member = await membersService.getByEmailWithSubscriptions(emailAddress);
    const subscription = member.subscriptions[0];

    if (!subscription) {
        throw new Error(`Expected redeemed offer member ${emailAddress} to have a subscription`);
    }

    return {
        accountPage,
        emailAddress,
        name,
        subscription
    };
}

View on GitHub (pinned to 47d8b0e2ad)

Solutions

  1. Add a short retry/poll loop around `getByEmailWithSubscriptions` to wait for webhook processing to persist the subscription.
  2. Confirm the Stripe webhook client in the test environment is delivering `customer.subscription.created` events.
  3. Verify the success URL navigation actually triggered Ghost's subscription-creation endpoint.
  4. Check that the offer is configured to grant a paid tier subscription, not just a discount.

Example fix

// before
const member = await membersService.getByEmailWithSubscriptions(emailAddress);
const subscription = member.subscriptions[0];
if (!subscription) {
    throw new Error(`Expected redeemed offer member ${emailAddress} to have a subscription`);
}

// after — wait for webhook to land
let member = await membersService.getByEmailWithSubscriptions(emailAddress);
for (let i = 0; i < 20 && !member.subscriptions?.length; i++) {
    await new Promise(r => setTimeout(r, 500));
    member = await membersService.getByEmailWithSubscriptions(emailAddress);
}
const subscription = member.subscriptions?.[0];
if (!subscription) {
    throw new Error(`Expected redeemed offer member ${emailAddress} to have a subscription`);
}
Defensive patterns

Strategy: retry

Type guard

function memberHasSubscription(m: unknown): m is {subscriptions: unknown[]} {
    return !!m && Array.isArray((m as any).subscriptions) && (m as any).subscriptions.length > 0;
}

Try / catch

let member = await membersService.getByEmailWithSubscriptions(emailAddress);
for (let i = 0; i < 20 && !memberHasSubscription(member); i++) {
    await new Promise(r => setTimeout(r, 500));
    member = await membersService.getByEmailWithSubscriptions(emailAddress);
}
if (!memberHasSubscription(member)) {
    throw new Error(`Expected redeemed offer member ${emailAddress} to have a subscription`);
}

Prevention

When it happens

Trigger: Offer redemption navigated to the success URL, but `member.subscriptions` is empty or undefined when re-fetched from the members API. Typically a Stripe webhook (`invoice.paid` / `customer.subscription.created`) did not arrive or was not processed before the lookup.

Common situations: Stripe webhook delivery is slow or dropped in the test environment; the member was created but the subscription is still pending in Stripe; race between `page.goto(successUrl)` completing and backend subscription persistence; webhook signing secret mismatch.

Related errors


AI-assisted analysis of TryGhost/Ghost@47d8b0e2ad (2026-08-13). Data as JSON: /api/errors/cf9ac06a8b2c1b32. Report an issue: GitHub.