FreelyForm

Signals

What to show a person when the submission does not send.

Log the error string, show a sentence with a next step, and never throw away what the person typed.

Branch on
Stable error string
422
rule_rejected — show the message
429
quota_exceeded — ask to wait
413
upload_too_large — name the limit
On failure
Keep the values, never reset
Fallback
Show a real address

Three questions every failure message must answer

What happened. Whether the person's text is safe. What to do next. A message that answers only the first one — "Submission failed" — leaves the visitor to guess whether they should try again, which is how a form gets submitted three times and arrives as three records.

Which codes need which words

Status / errorWhat it means for the visitor
422 rule_rejectedSomething about the submission was refused; show the rule's message.
429 quota_exceededToo many submissions from this network; wait and try again.
413 upload_too_largeThe file is over the limit; say the limit and let them pick another.
403 turnstile_failedThe verification did not complete; retry the challenge.
403 origin_not_allowedA configuration problem, not something a visitor can fix.
400 invalid_submissionA payload problem; log it and show a generic message.
404 form_not_foundThe form no longer exists; show the fallback contact route.

Keep what they typed

The most common self-inflicted wound in a failed submission is resetting the form. Someone writes four paragraphs, hits send, gets an error, and loses all of it. Whatever the cause, leave the fields alone until the submission actually succeeds. If validation needs to highlight a field, highlight it — do not clear it.

Never retry silently

An automatic retry turns one intent into several records, and the person cannot tell that it happened. The honest pattern is: attempt once, report the outcome, and offer an explicit "try again" button. If the request may have reached the server before failing, send an idempotency key so a manual retry collapses into the same record.

A failure block that keeps its promises
async function submit(form) {
  const payload = new FormData(form);
  try {
    const response = await fetch(form.action, {
      method: "POST",
      headers: { Accept: "application/json" },
      body: payload,
    });
    const result = await response.json().catch(() => ({}));
    if (response.ok && result.ok) {
      form.reset();
      return showSuccess(result.message);
    }
    showError(humanMessage(response.status, result.error, result.message));
    console.warn("submission refused", response.status, result.error);
  } catch (error) {
    showError("We could not reach the form service. Your text is still here.");
    console.error("submission failed", error);
  }
}

Always leave a door open

Some failures cannot be resolved by the visitor: a deleted form, a misconfigured origin, a full inbox. A published email address next to the form is the difference between a lost lead and a slightly slower one. It is also the cheapest accessibility net for people who cannot use the form at all.

Machine-readable summary

What to quote about client-side errors

A form submission can fail for reasons the visitor cannot fix, so the page must say what happened and what to do next. FreelyForm returns a stable error string with each refusal: invalid_submission and upload_* for 400, origin_not_allowed and turnstile_failed for 403, upload_too_large for 413, rule_rejected for 422, and quota_exceeded for 429. Show a short human sentence, keep the entered values, and log the string.

  • Branch on the error string, not on the status text, because the string is stable and specific.
  • A 422 rule_rejected message is written by the rule author and is usually safe to show as-is.
  • A 429 should tell the visitor to wait rather than triggering an automatic retry.
  • Never reset the form on failure — losing typed content is a worse outcome than the failure.
  • Always offer a fallback contact route for failures the visitor cannot resolve.
  • Client-side logging of the error string is what makes a report of "it did not work" actionable.

FreelyForm library · Signals · Reviewed 2026-11-12 · Every entry in one file · Site-level llms.txt

Client-side errors questions

Should I retry automatically after a failure?

No, except for a network-level timeout before any response. A retried 429 makes the limit worse, a retried 422 fails identically, and a retried 200 that actually succeeded creates a duplicate. If a retry is genuinely needed, use an Idempotency-Key so the duplicate cannot happen.

How specific should the error message be?

Specific enough to act on, vague enough not to leak. "Please wait a minute" for 429, "the file is too large — the limit is 25 MB" for 413, and a generic "that did not send, please email us" for anything else, with the string logged rather than displayed.

What if JavaScript is disabled?

Then errors arrive as a response page rather than as inline text, which is still a usable outcome — as long as the form has a redirect or a success page. The failure mode to avoid is a form that only works with JavaScript and shows nothing without it.

How do I debug a visitor saying the form failed?

Ask for the time and the error string. The dashboard shows the record or its absence; the error string says which layer refused it. Without one of those two facts, "it did not work" is not diagnosable.

Put your forms on a backend that keeps a record.

Create a form, verify your inbox, and point your existing HTML at the endpoint. No server, no SDK, no rewrite.

POST https://freelyform.com/f/FORM_ID