antiwork/gumroad · error · ResponseError
responseData.error_message
Error message
responseData.error_message
What it means
createDiscount POSTs checkout_discounts_path; typia.assert narrows the reply, and the { success: false, error_message } branch throws ResponseError carrying the server's error_message. This is the endpoint's validation channel: the discount failed to save and e.message states why in the server's own words.
Source
Thrown at app/javascript/data/offer_code.ts:141
duration_in_billing_cycles: payload.durationInBillingCycles,
minimum_amount_cents: payload.minimumAmount,
once_per_cart: payload.discount.type === "cents" && payload.oncePerCart,
existing_customers_only: payload.existingCustomersOnly,
ownership_product_ids: payload.existingCustomersOnly ? payload.ownershipProductIds : [],
ownership_duration_tiers: payload.ownershipDurationTiers,
});
export const createDiscount = async (payload: DiscountPayload) => {
const response = await request({
method: "POST",
accept: "json",
url: Routes.checkout_discounts_path(),
data: buildDiscountPayload(payload),
});
const responseData = typia.assert<
{ success: true; offer_codes: OfferCode[]; pagination: PaginationProps } | { success: false; error_message: string }
>(await response.json());
if (!responseData.success) throw new ResponseError(responseData.error_message);
return responseData;
};
export const updateDiscount = async (id: string, payload: DiscountPayload) => {
const response = await request({
method: "PUT",
accept: "json",
url: Routes.checkout_discount_path(id),
data: buildDiscountPayload(payload),
});
const responseData = typia.assert<
{ success: true; offer_codes: OfferCode[]; pagination: PaginationProps } | { success: false; error_message: string }
>(await response.json());
if (!responseData.success) throw new ResponseError(responseData.error_message);
return responseData;
};
export const deleteDiscount = async (id: string) => {View on GitHub (pinned to afeacbd394)
Solutions
- Show e.message next to the offending form field — the server text names the problem.
- Pre-validate client-side: non-empty code, percent 1–100, at least one product selected when not universal.
- On a code collision, pick a different code rather than resubmitting.
- Verify excludedProductIds and selectedProductIds don't intersect.
Example fix
// before
await createDiscount(payload);
// after — catch cheap mistakes before the POST
if (!payload.code.trim()) throw new Error('Enter a code.');
if (payload.discount.type === 'percent' && (payload.discount.value <= 0 || payload.discount.value > 100)) throw new Error('Percent off must be between 1 and 100.');
if (!payload.universal && payload.selectedProductIds.length === 0) throw new Error('Select at least one product.');
await createDiscount(payload); Defensive patterns
Strategy: validation
Validate before calling
if (!payload.code.trim()) throw new Error('Enter a code.');
if (payload.discount.type === 'percent' && (payload.discount.value <= 0 || payload.discount.value > 100)) throw new Error('Percent off must be between 1 and 100.');
if (!payload.universal && payload.selectedProductIds.length === 0) throw new Error('Select at least one product.');
if (payload.selectedProductIds.some((id) => payload.excludedProductIds.includes(id))) throw new Error('A product cannot be both selected and excluded.'); Type guard
const isDiscountFailure = (json: unknown): json is { success: false; error_message: string } =>
typeof json === 'object' && json !== null && (json as { success?: unknown }).success === false && typeof (json as { error_message?: unknown }).error_message === 'string'; Try / catch
try {
const result = await createDiscount(payload);
} catch (e) {
assertResponseError(e);
showFieldError(e.message); // server's error_message names the offending rule
} Prevention
- Pre-validate code format, percent bounds, and product selection before the POST.
- Show error_message at the form field level, not as a toast that scrolls away.
- Suggest a unique code when the server reports a collision.
- Keep client validation in sync with the server's discount rules.
When it happens
Trigger: Offer code already used by another discount; percent value outside 1–100; a non-universal discount with empty selectedProductIds; excluded products overlapping selected ones; currencyCode missing for a universal discount.
Common situations: Copying an existing code name; creating a universal percent discount without a currency; excluding variants while selecting their parent product.
Related errors
- ${responseData.error}
- responseData.error_message
- ${data.error}
- No recovery codes were generated. Please try again.
- Offer code cannot be expired
AI-assisted analysis of antiwork/gumroad@afeacbd394 (2026-08-21).
Data as JSON: /api/errors/8718bc7b932049db.
Report an issue: GitHub.