# Form submission retries on network failure

- **Pattern:** `ab-001290` (`error-resilience.graceful-degradation-shutdown.form-submission-retry`)
- **Severity:** low
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001290
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001290)

## Why it matters

A form that clears its fields on network failure destroys user work and guarantees the user abandons the operation — or at minimum re-enters all data under frustration. ISO 25010 reliability.recoverability requires the application to preserve state across failure events. For signup, checkout, and contact forms this is a direct conversion killer: a single network timeout during form submission can permanently lose a customer. The fix is architectural, not cosmetic: form state must live in React state or a form library, never be cleared in the catch block.

## Severity rationale

Low severity at the technical level, but high business impact — form abandonment after submission failure is a direct revenue loss vector for signup and checkout flows.

## Remediation

Never clear form state in error handlers. Keep field values in state, show an inline error, and expose a retry action that resubmits the preserved data.

```tsx
// components/contact-form.tsx
const [fields, setFields] = useState({ email: '', message: '' })
const [status, setStatus] = useState<'idle' | 'submitting' | 'error' | 'success'>('idle')

async function handleSubmit(e: React.FormEvent) {
  e.preventDefault()
  setStatus('submitting')
  try {
    await submitContact(fields)
    setStatus('success')
  } catch {
    setStatus('error') // fields unchanged — user can retry
  }
}

// In the render:
{status === 'error' && (
  <p>
    Submission failed. <button onClick={handleSubmit}>Try again</button>
  </p>
)}
```

For long forms, consider persisting draft state to `localStorage` so data survives a hard refresh.

## Detection

- **ID:** `form-submission-retry`
- **Severity:** `low`
- **What to look for:** Count all form submission handlers. Enumerate which handle submission failures with retry options vs. which silently fail or lose user input. Look at form submission handlers. Check whether they preserve user input on network failure and offer a retry option without re-entering data.
- **Pass criteria:** Form submission preserves user input on network failure; user can retry or edit input before resubmitting. At least 80% of form submissions must preserve user input on failure and offer a retry mechanism.
- **Fail criteria:** Form submission on network failure loses user input or offers no retry mechanism.
- **Skip (N/A) when:** The application has no forms.

- **Cross-reference:** For retry logic with backoff, see `retry-logic-backoff`.
- **Detail on fail:** `"Network failure during form submission clears all user input; user must re-enter data"` or `"No retry option after form submission failure"`
- **Remediation:** Preserve form state and offer retry:

  ```tsx
  // components/contact-form.tsx — form with retry
  const [error, setError] = useState(false)
  const onSubmit = async (data: FormData) => { try { await submitForm(data) } catch { setError(true) } }
  {error && <p>Submission failed. <button onClick={() => onSubmit(formData)}>Try Again</button></p>}
  ```

  ```tsx
  export function SubmitForm() {
    const [formData, setFormData] = useState({ email: '', message: '' })
    const [submitting, setSubmitting] = useState(false)
    const [error, setError] = useState(null)

    async function handleSubmit(e: React.FormEvent) {
      e.preventDefault()
      setSubmitting(true)
      setError(null)

      try {
        const response = await fetch('/api/submit', {
          method: 'POST',
          body: JSON.stringify(formData),
        })
        if (!response.ok) throw new Error('Submission failed')
        setFormData({ email: '', message: '' })
      } catch (err) {
        setError(err)
        // User data is preserved in formData state
      } finally {
        setSubmitting(false)
      }
    }

    return (
      <form onSubmit={handleSubmit}>
        <input
          value={formData.email}
          onChange={(e) => setFormData({ ...formData, email: e.target.value })}
          placeholder="Email"
        />
        <textarea
          value={formData.message}
          onChange={(e) => setFormData({ ...formData, message: e.target.value })}
          placeholder="Message"
        />
        {error && (
          <div>
            <p>Error: {error.message}</p>
            <button type="submit" disabled={submitting}>
              {submitting ? 'Retrying...' : 'Retry'}
            </button>
          </div>
        )}
        {!error && (
          <button type="submit" disabled={submitting}>
            {submitting ? 'Submitting...' : 'Submit'}
          </button>
        )}
      </form>
    )
  }
  ```

## External references

- iso-25010 reliability.recoverability

Taxons: error-resilience, user-experience

HTML version: https://auditbuffet.com/patterns/ab-001290
