DEV Community

Logixer Creatives
Logixer Creatives

Posted on

Your Contact Form Said “Submitted”—But the Lead Was Lost

A developer’s reliability checklist for building forms that validate correctly, survive failures and never show success too early

A contact form is often one of the most commercially important features on a service website.

It may contain only a few fields and a submit button, but behind that simple interface is a chain of events:

  1. The browser validates the information.
  2. JavaScript sends the request.
  3. The server validates it again.
  4. The submission is stored or processed.
  5. A notification is sent.
  6. The user receives confirmation.
  7. The marketing or sales team receives the enquiry.

If any step fails silently, the visitor may see a success message even though the business never receives the lead.

A reliable form should therefore be treated as a transaction—not simply as a visual component.

The Most Dangerous Form Failure

A clear error message is inconvenient, but it is visible. The visitor knows that something went wrong and can try again.

The more dangerous failure is a false success message.

Consider this frontend code:

form.addEventListener("submit", async (event) => {
  event.preventDefault();

  await fetch("/api/contact", {
    method: "POST",
    body: new FormData(form),
  });

  showSuccessMessage();
});
Enter fullscreen mode Exit fullscreen mode

At first glance, this appears reasonable. The form waits for fetch() and then displays confirmation.

However, fetch() does not reject its promise simply because the server returns an HTTP error such as 400, 404 or 500. The response must be checked explicitly.

The server could return 500 Internal Server Error, and the interface would still show success.

A safer version begins by checking the response:

form.addEventListener("submit", async (event) => {
  event.preventDefault();

  try {
    const response = await fetch("/api/contact", {
      method: "POST",
      body: new FormData(form),
    });

    if (!response.ok) {
      throw new Error(`Submission failed: ${response.status}`);
    }

    showSuccessMessage();
  } catch (error) {
    showErrorMessage(
      "We could not send your enquiry. Please try again."
    );
  }
});
Enter fullscreen mode Exit fullscreen mode

The success state should appear only after the server confirms that the submission was accepted.

1. Define What “Success” Actually Means

Before writing code, decide what must happen before the API returns success.

Possible definitions include:

  • The enquiry passed server-side validation.
  • The enquiry was saved in the database.
  • A message was added to a reliable processing queue.
  • A CRM record was created.
  • A notification email was successfully handed to an email provider.

These are not equivalent.

For example, an application may save the enquiry correctly but fail to send the notification email. The lead still exists and can be recovered if administrators have a dashboard or database record.

If the application depends only on email and that email fails, the enquiry may disappear completely.

A practical design is:

Receive request
      ↓
Validate input
      ↓
Store enquiry
      ↓
Return accepted response
      ↓
Send notifications asynchronously
Enter fullscreen mode Exit fullscreen mode

The permanent record should not depend on an email notification completing successfully.

2. Validate in Both the Browser and Server

Browser validation improves usability. It can quickly identify missing or incorrectly formatted values before a request is sent.

Example:

<form id="contact-form">
  <label for="name">Name</label>
  <input
    id="name"
    name="name"
    type="text"
    minlength="2"
    maxlength="100"
    required
  >

  <label for="email">Email</label>
  <input
    id="email"
    name="email"
    type="email"
    required
  >

  <label for="message">Requirement</label>
  <textarea
    id="message"
    name="message"
    minlength="10"
    maxlength="2000"
    required
  ></textarea>

  <button type="submit">Send enquiry</button>
</form>
Enter fullscreen mode Exit fullscreen mode

HTML constraint validation can check requirements such as field type, minimum length, maximum length and patterns.

However, browser validation is not a security boundary. Requests can be modified or sent directly to the API without using the page.

The server must validate every field again.

Server validation should check:

  • Required fields
  • Expected data types
  • Minimum and maximum lengths
  • Accepted formats
  • Allowed values
  • Relationships between fields
  • Unexpected additional properties
  • Request-size limits

OWASP recommends validating both syntax—whether data has the correct structure—and semantics—whether the submitted value makes sense for the operation.

Do not rely on denylisting a few suspicious characters. Define what the application accepts, use parameterised database queries and encode output appropriately.

3. Prevent Accidental Duplicate Submissions

Users may click the submit button repeatedly if the response is slow. Mobile connections can make this especially common.

Disable the button while the request is in progress:

async function submitForm(form, button) {
  button.disabled = true;
  button.textContent = "Sending…";

  try {
    const response = await fetch("/api/contact", {
      method: "POST",
      body: new FormData(form),
    });

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }

    showSuccessMessage();
    form.reset();
  } catch (error) {
    showErrorMessage("Submission failed. Please try again.");
  } finally {
    button.disabled = false;
    button.textContent = "Send enquiry";
  }
}
Enter fullscreen mode Exit fullscreen mode

A disabled button cannot be clicked, focused or submitted with the form, so use it carefully and restore it after both successful and failed requests.

Frontend protection alone is not enough. Two requests can still reach the server because of retries, multiple tabs or network behaviour.

For important forms, send an idempotency key or submission identifier:

const submissionId = crypto.randomUUID();

const response = await fetch("/api/contact", {
  method: "POST",
  headers: {
    "Idempotency-Key": submissionId,
  },
  body: new FormData(form),
});
Enter fullscreen mode Exit fullscreen mode

The server can store the key and reject or reuse the result of a duplicate request instead of creating two enquiries.

4. Separate Validation Errors From System Failures

Not every failure should display the same message.

The API should return meaningful status codes:

201 Created        Submission accepted
400 Bad Request    Malformed request
422 Unprocessable  Field validation failed
429 Too Many Requests
500 Server Error   Unexpected backend failure
503 Unavailable    Temporary service problem
Enter fullscreen mode Exit fullscreen mode

A structured validation response might look like this:

{
  "message": "Please correct the highlighted fields.",
  "errors": {
    "email": "Enter a valid email address.",
    "message": "Enter at least 10 characters."
  }
}
Enter fullscreen mode Exit fullscreen mode

The frontend can display each message beside the relevant field.

System failures should use a general message that helps the visitor recover:

We could not send your enquiry right now.
Please try again or contact us by telephone.
Enter fullscreen mode Exit fullscreen mode

Do not expose stack traces, database information, API credentials or internal filenames to the browser.

5. Add Timeouts and Clear Recovery States

A request can remain pending because of a slow network, overloaded server or unavailable third-party service.

Use AbortController to stop waiting after a reasonable period:

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 15000);

try {
  const response = await fetch("/api/contact", {
    method: "POST",
    body: new FormData(form),
    signal: controller.signal,
  });

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }

  showSuccessMessage();
} catch (error) {
  if (error.name === "AbortError") {
    showErrorMessage(
      "The request took too long. Please check your connection and try again."
    );
  } else {
    showErrorMessage(
      "We could not send your enquiry. Please try again."
    );
  }
} finally {
  clearTimeout(timeoutId);
}
Enter fullscreen mode Exit fullscreen mode

Be careful with automatic retries. Retrying a POST request without duplicate protection may create multiple records.

It is often safer to let the visitor retry manually while the backend uses a unique submission identifier.

6. Protect the Endpoint Without Punishing Genuine Users

Public forms attract spam and automated submissions, but aggressive protection can block real customers.

A balanced system may combine:

  • Server-side rate limiting
  • A hidden honeypot field
  • Minimum completion-time checks
  • Duplicate-content detection
  • IP and behaviour signals
  • CAPTCHA only when risk is detected
  • Email or telephone validation where appropriate

Security controls should be added on the server. Hiding JavaScript logic or renaming fields does not provide reliable protection.

If the form uses authenticated sessions or cookies, implement appropriate CSRF protection. OWASP recommends server-generated CSRF tokens or other suitable protections depending on the application architecture.

7. Make Every Form State Accessible

A reliable form must communicate its status to people using keyboards and assistive technologies.

Ensure that:

  • Every field has a visible label.
  • Error messages identify the affected field.
  • Keyboard focus moves to the first invalid field when appropriate.
  • Colour is not the only error indicator.
  • The loading state is communicated clearly.
  • Success and failure messages are announced.
  • The submit button has an understandable label.

A status container can announce updates:

<div
  id="form-status"
  role="status"
  aria-live="polite"
></div>
Enter fullscreen mode Exit fullscreen mode

Update it only with useful messages:

statusElement.textContent =
  "Your enquiry was received successfully.";
Enter fullscreen mode Exit fullscreen mode

Do not replace the entire form immediately if doing so causes focus to disappear. Move focus deliberately to the confirmation heading or keep the form structure stable.

8. Track Confirmed Outcomes, Not Button Clicks

A submit-button click is not a completed enquiry.

If analytics records a conversion before the server confirms success, reports can include:

  • Validation failures
  • Spam submissions
  • Network errors
  • Server errors
  • Repeated clicks
  • Abandoned requests

Trigger the conversion event only after receiving a confirmed success response:

if (response.ok) {
  analytics.track("contact_form_completed", {
    form: "main_contact",
  });

  showSuccessMessage();
}
Enter fullscreen mode Exit fullscreen mode

Avoid sending personal information such as names, email addresses, telephone numbers or message contents into analytics platforms unless the platform and your privacy policy explicitly support that use.

9. Log the Entire Submission Journey

When a lead is reported missing, developers need enough information to determine what happened.

Useful server logs may include:

  • Timestamp
  • Request or correlation ID
  • Form identifier
  • Validation result
  • Response status
  • Storage result
  • Notification result
  • CRM or webhook result
  • Processing duration

Do not write full sensitive form contents into general application logs.

A correlation ID can connect the browser response, API request, database record and notification job:

{
  "success": true,
  "submissionId": "f8e70b38-74de-4f49-ae57-6f51ed3ec438"
}
Enter fullscreen mode Exit fullscreen mode

This makes support investigations much easier than searching by approximate time or visitor name.

10. Test Failures Before Launch

A form is not fully tested when only the successful path works.

Verify these scenarios:

  • Required field missing
  • Invalid email format
  • Very long message
  • Unexpected additional field
  • Double-clicked submit button
  • Slow network
  • Disconnected network
  • API returns 400
  • API returns 422
  • API returns 429
  • API returns 500
  • Request times out
  • Database write fails
  • Email notification fails
  • CRM integration fails
  • JavaScript is unavailable
  • Screen reader announces the result
  • Form works on a real mobile device

Also submit a genuine test enquiry and confirm that it appears in the system used by the sales team.

A Reliable Contact-Form Checklist

Before deployment, confirm that:

  • The form uses semantic HTML.
  • Browser validation improves usability.
  • The server validates every field.
  • response.ok or the response status is checked.
  • Success appears only after server confirmation.
  • Duplicate submissions are controlled.
  • Enquiries are stored before notifications are sent.
  • Spam and rate limits are handled server-side.
  • Errors provide a clear recovery path.
  • Form states are accessible.
  • Analytics records confirmed submissions.
  • Sensitive data is not exposed in logs.
  • Every failure scenario has been tested.
  • Monitoring can detect submission failures.

Treat Every Enquiry Like a Transaction

A contact form is not finished when the button looks correct or the success animation appears.

The complete system must validate the request, store it reliably, communicate failures honestly and provide enough monitoring to investigate problems.

At Logixer Creatives, conversion-focused website work includes the technical journey that happens after a visitor clicks “Submit,” not only the design visible before it.

A form earns trust when its message is accurate. If it says an enquiry was received, the system should be able to prove it.

Top comments (1)

Collapse
 
mohith_kumar_05846f3211f3 profile image
Mohith kumar •

Good checklist. Silent failure after a green "Submitted" is the worst kind of bug because nobody reports it. One thing I'd add from building chatform.in: save answers as the person goes, not only on the final submit, so a failed last request doesn't lose the whole lead and you can still follow up with people who stopped halfway. Do you also recommend a scheduled test submission as a canary?