TryGhost/Ghost · error · Error
result.error.message
Error message
result.error.message
What it means
Thrown in apps/portal/src/data-attributes.js update-billing flow when Stripe.js redirectToCheckout (used here to enter the billing portal session) resolves with result.error populated. The thrown error message is t(result.error.message), feeding the Stripe message through Portal's i18n lookup (which usually returns the original string because no key matches).
Source
Thrown at apps/portal/src/data-attributes.js:283
body: JSON.stringify({
identity: identity,
successUrl: successUrl,
cancelUrl: cancelUrl
})
}).then(function (res) {
if (!res.ok) {
throw new Error(t('Could not create Stripe checkout session'));
}
return res.json();
});
}).then(function (result) {
let stripe = window.Stripe(result.publicKey);
return stripe.redirectToCheckout({
sessionId: result.sessionId
});
}).then(function (result) {
if (result.error) {
throw new Error(t(result.error.message));
}
}).catch(function (err) {
console.error(err);
el.addEventListener('click', clickHandler);
el.classList.remove('loading');
if (errorEl) {
errorEl.innerText = err.message;
}
el.classList.add('error');
});
}
el.addEventListener('click', clickHandler);
});
Array.prototype.forEach.call(document.querySelectorAll('[data-members-manage-billing]'), function (el) {
let errorEl = el.querySelector('[data-members-error]');
let membersReturn = el.dataset.membersReturn;
let returnUrl;View on GitHub (pinned to 47d8b0e2ad)
Solutions
- Surface result.error.message directly to the user — wrapping it in t() often returns the English original anyway.
- Have the user click the edit-billing button again (the catch re-binds the handler) to mint a fresh session.
- Verify the publishable key returned by the backend matches the connected Stripe account.
- Ask the user to disable content blockers that interfere with js.stripe.com.
Example fix
// before
if (result.error) {
throw new Error(t(result.error.message));
}
// after: don't route a dynamic Stripe message through i18n key lookup
if (result.error) {
throw new Error(result.error.message);
} Defensive patterns
Strategy: try-catch
Type guard
function isStripeRedirectError(result) {
return result?.error != null && typeof result.error.message === 'string';
} Try / catch
try {
const result = await stripe.redirectToCheckout({sessionId});
if (isStripeRedirectError(result)) { throw new Error(result.error.message); }
} catch (err) {
showError(el, err.message);
el.classList.remove('loading');
el.addEventListener('click', clickHandler);
} Prevention
- Do not pass dynamic Stripe messages through t() — they are not i18n keys.
- Re-mint the billing session on each retry.
- Verify the publishable key matches the connected Stripe account.
- Surface redirectResult.error.message directly to the user.
When it happens
Trigger: create-stripe-update-session returned a valid sessionId/publicKey; window.Stripe(publicKey).redirectToCheckout({sessionId}) resolves with {error:{message}} — Stripe.js could not perform the redirect (session expired, key mismatch, browser blocked).
Common situations: Customer portal session expired before redirect completed; publishable key returned by the backend differs from the account that created the session; browser blocking third-party cookies / popups; network interruption during redirect.
Related errors
- redirectResult.error.message
- Could not create Stripe checkout session
- redirectResult.error.message
- Failed to continue gift subscription, please try again.
- Failed to process gift checkout, please try again.
AI-assisted analysis of TryGhost/Ghost@47d8b0e2ad (2026-08-13).
Data as JSON: /api/errors/1ef10b3641d316d4.
Report an issue: GitHub.