antiwork/gumroad · error · ResponseError

${responseData.error_message}

Error message

${responseData.error_message}

What it means

CreateBrandAccountModal POSTs the new brand account (name, username, email, payout-porting flag). The server replies with a discriminated union { success: true } | { success: false; error_message } and the throw passes the server's error_message straight through as the ResponseError message, shown via showAlert. typia.assert enforces the union shape — a body matching neither branch throws TypiaError, which assertResponseError in the catch re-throws (uncaught by this handler).

Source

Thrown at app/javascript/components/CreateBrandAccountModal.tsx:68

    setIsSubmitting(true);
    try {
      const response = await request({
        method: "POST",
        accept: "json",
        url: Routes.sellers_brand_accounts_path(),
        data: {
          brand_account: {
            name,
            username,
            email,
            use_existing_payout_setup: canPortPayoutSetup && useExistingPayoutSetup,
          },
        },
      });
      const responseData = typia.assert<{ success: true } | { success: false; error_message: string }>(
        await response.json(),
      );
      if (!responseData.success) throw new ResponseError(responseData.error_message);
      // The session is already switched into the new brand account; land on its dashboard.
      window.location.href = Routes.dashboard_path();
    } catch (e) {
      assertResponseError(e);
      showAlert(e.message, "error");
      setIsSubmitting(false);
    }
  };

  return (
    <Modal
      open={open}
      title="Create a new Gumroad"
      onClose={onClose}
      footer={
        <>
          <Button onClick={onClose} disabled={isSubmitting}>
            Cancel

View on GitHub (pinned to afeacbd394)

Solutions

  1. Read the alert — it carries the server's exact rejection reason.
  2. If username/email rejections are frequent, validate format client-side and check availability before submit.
  3. If error_message looks truncated or generic, check the controller for what it returns per failure branch.
  4. If nothing shows and the error boundary triggers instead, the body did not match the union — log the raw response once.
  5. Keep the fields in the POST in sync with the server's strong params; a renamed param yields success:false for a confusing reason.
Defensive patterns

Strategy: validation

Validate before calling

const USERNAME = /^[a-z0-9_-]{3,30}$/i;
const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

if (!USERNAME.test(username) || !EMAIL.test(email)) {
  showAlert('Check the account name and email before continuing.', 'error');
  return;
}

Type guard

type BrandAccountResult = { success: true } | { success: false; error_message: string };
const isBrandAccountResult = (v: unknown): v is BrandAccountResult =>
  typeof v === 'object' && v !== null && typeof (v as { success?: unknown }).success === 'boolean';

Try / catch

try {
  await createBrandAccount();
} catch (e) {
  assertResponseError(e);
  showAlert(e.message, 'error'); // server's error_message — e.g. 'that username is taken'
  setIsSubmitting(false); // always re-enable the button
}

Prevention

When it happens

Trigger: POST returning success:false with error_message: username already taken, email invalid or already in use, payout-setup porting check failed — i.e. classic form-validation rejections delivered in the response body rather than as a 422.

Common situations: Seller picks a username that is taken; brand email collides with an existing account; payout porting rejected because the source account is not fully set up; server validation changed but the modal still submits stale fields.

Related errors


AI-assisted analysis of antiwork/gumroad@afeacbd394 (2026-08-21). Data as JSON: /api/errors/0bca98d3c1ea8808. Report an issue: GitHub.