A thank-you page that lied

When a lead failed to save, visitors still saw “thank you” and the ad platforms still counted a conversion. How I found it, and the check now built into every form.

27 Oct 2026 2 min readCase study · Website & search programme

The problem

Failed leads still showed “thank you” and counted as ad conversions.

The solution

Every form now confirms the lead saved, with tests that force each failure.

7 / 7forms verify the save
21 / 21failure-mode tests pass
90 daysattribution, going live

There are two ways a lead form can fail. It can fail loudly: an error, a red message, a visitor who tries again. Or it can fail politely: a cheerful thank-you page while the lead goes nowhere. The second kind is far worse, because it also teaches your ad platforms that the campaign is working.

Finding the polite failures

Two things didn’t add up. Ad clicks were arriving on two landing pages, yet those pages had recorded zero conversions for weeks. And on the main forms, the conversion counts and the CRM didn’t quite agree.

Failure one: a single apostrophe. Both broken landing pages had an apostrophe inside a single-quoted string in inline JavaScript. That one character was a syntax error, so the whole script never ran. The form looked fine and submitted nowhere.

Failure two: “200 OK”, with an error inside. The lead endpoint passes submissions on to the CRM. When the CRM call failed, it still replied HTTP 200 and put the error in the response body. Our forms checked only the status code. Visitors saw “thank you”, Google Ads counted a conversion, and no lead existed.

The fixes

Trust the body, not the status. All seven form implementations now call one shared check before showing success:

// Simplified: a real save is one where the response body confirms it,
// whatever the status code says.
async function leadSaved(res) {
  if (!res.ok) return false;
  const body = await res.json().catch(() => null);
  return Boolean(body && body.success === true && !body.error);
}

If the save didn’t happen, the visitor gets a clear retry message and a one-tap WhatsApp fallback carrying their details. The conversion event fires only after a confirmed save.

Parse-check every inline script. Before each deploy, every inline <script> is run through new Function(). Anything that doesn’t parse blocks the release. That one line would have caught the apostrophe.

Test the failure, not just the happy path. A Playwright suite intercepts the network request and forces each failure mode: a 500, a 200 with an error body, a timeout, a malformed response. It then checks that the page shows the right thing and fires the right event. 21 of 21 cases pass.

Keep the attribution. UTM tags and Google click IDs used to live only in session storage, so they were gone by the time a visitor came back to enquire. The fix, built and tested and now going live, keeps them for 90 days, falls back to the referrer, and writes them into both CRM fields.

What changed

  • Ad conversions now mean saved leads, so the bidding optimises for real enquiries.
  • Two paid landing pages started converting again the day the fix shipped.
  • A broken script can no longer reach production silently.

Takeaway

Success screens are claims. Before you show one, prove it, ideally with a test that tries hard to make it lie.

More problems, more solutions.

All posts

Non-brand clicks are the SEO number that matters

+183% non-brand clicks

A path-traced showroom in a TV browser

22 renders, path-traced offline in about 2 hours