overleaf/overleaf · error · DuplicateAddOnError
Subscription already has add-on
Error message
Subscription already has add-on
What it means
PaymentProviderEntities.getRequestForAddOnPurchase builds a Recurly subscription-change request to purchase an add-on. Before building it, it checks this.hasAddOn(code); if the add-on already exists on the subscription, a DuplicateAddOnError is thrown with the subscriptionId and addOnCode, because Recurly cannot add an add-on that is already present.
Source
Thrown at services/web/app/src/Features/Subscription/PaymentProviderEntities.mjs:219
})
return changeRequest
}
/**
* Purchase an add-on on this subscription
*
* @param {string} code
* @param {number} [quantity]
* @param {number} [unitPrice]
* @return {PaymentProviderSubscriptionChangeRequest} - the change request to send to
* Recurly
*
* @throws {DuplicateAddOnError} if the add-on is already present on the subscription
*/
getRequestForAddOnPurchase(code, quantity = 1, unitPrice) {
if (this.hasAddOn(code)) {
throw new DuplicateAddOnError('Subscription already has add-on', {
subscriptionId: this.id,
addOnCode: code,
})
}
const baseAddOns = this.#getBaseAddOnsForChangeRequest('now')
const addOnUpdates = baseAddOns.map(addOn => addOn.toAddOnUpdate())
addOnUpdates.push(
new PaymentProviderSubscriptionAddOnUpdate({ code, quantity, unitPrice })
)
return new PaymentProviderSubscriptionChangeRequest({
subscription: this,
timeframe: 'now',
addOnUpdates,
})
}
/**View on GitHub (pinned to 28ad3b03b7)
Solutions
- Check this.hasAddOn(code) (or the equivalent entitlement) before calling getRequestForAddOnPurchase
- If the add-on exists, use getRequestForAddOnUpdate to change its quantity instead of purchasing
- Catch DuplicateAddOnError and treat it as a no-op / idempotent success
- Re-fetch the subscription entity before building the change request so pending changes are reflected
Example fix
// before
const request = subscription.getRequestForAddOnPurchase('additional-license', 1)
// after
const request = subscription.hasAddOn('additional-license')
? subscription.getRequestForAddOnUpdate('additional-license', newQuantity)
: subscription.getRequestForAddOnPurchase('additional-license', 1) Defensive patterns
Strategy: try-catch
Validate before calling
if (subscription.hasAddOn('additional-license')) {
// use update path instead of purchase
} Type guard
function canPurchaseAddOn(subscription, code) {
return typeof code === 'string' && !subscription.hasAddOn(code)
} Try / catch
try {
const request = subscription.getRequestForAddOnPurchase(code, qty)
} catch (err) {
if (err instanceof DuplicateAddOnError) {
return { status: 'already-owned', subscriptionId: err.info.subscriptionId }
}
throw err
} Prevention
- Always check hasAddOn(code) before purchase requests
- Make purchase flows idempotent — treat duplicate as success
- Refresh the subscription entity before building change requests
- Use the update path for quantity changes on existing add-ons
When it happens
Trigger: Calling getRequestForAddOnPurchase(code) for an add-on code already attached to the subscription (or included in the pending change's add-on set), e.g. re-submitting a purchase request or attempting to add 'additional-license' when the user already has it.
Common situations: Double-submission of an upgrade UI form; logic that adds seats via the add-on path when the add-on already exists; syncing state where a prior pending change already added the add-on; code paths that don't check current entitlements before upgrading.
Related errors
- Subscription does not have add-on to remove
- Subscription does not have add-on to update
- Add-on is not pending cancellation
- Subtotal amount in cents exceeded error
- This account has no billing info
AI-assisted analysis of overleaf/overleaf@28ad3b03b7 (2026-09-03).
Data as JSON: /api/errors/72259d96c1c5f240.
Report an issue: GitHub.