Found in AI answers?Hire Theory Road
Theory Road.

Form Error but the Lead Saved? Check Your Analytics Calls.

A form that saves the lead and still tells the visitor it failed costs you duplicates, support emails and lost trust. Here is how an analytics call causes it, how to prove it in five minutes, and the pattern that keeps tracking out of the success path.

By October 18, 202610 min read

When a form saves the lead but the visitor sees an error, the cause is almost always one of five things: an analytics call (clarity, gtag, fbq) sits in the same try block as the success handling and throws, the vendor global is undefined because its snippet has not run yet, a consent tool or ad blocker kept the snippet from ever running, the tracking call runs before the success state is rendered, or a vendor callback you wait on never fires. The fix is the same in every case: render success first, then fire each tracking call in its own guarded wrapper that checks typeof window.x === "function" and catches its own errors, so analytics can never decide what the visitor sees.

A client-side form submit has two jobs. The business job is sending the data to your server and telling the visitor it worked. The measurement job is telling Clarity, GA4, Google Ads, Meta and the rest that a conversion happened. Analytics vendors expose a global function for the second job: window.clarity for Microsoft Clarity, gtag for the Google tag, fbq for the Meta pixel. When code calls one of those globals and it is not a function, JavaScript throws a TypeError, and whatever catch block surrounds that call runs.

What does this bug look like from the outside?

The lead is in your database or inbox, but the visitor saw "something went wrong" and often submitted again. We ran into this on one of our own lead forms. Leads were arriving server side, the confirmation emails went out, and some visitors still got the error message. The tell was duplicate submissions from the same person a minute apart, and a gap between leads received and conversions recorded in the ad platforms. Nothing was wrong with the API. A tracking call after the successful response threw, and the catch block treated it as a failed submit.

In our case the call was a Microsoft Clarity event fired right after the server returned success. It threw while Clarity was still loading, before the final window.clarity function was in place. That timing detail is our observation from debugging, not something Microsoft documents. The class of bug is general: any analytics call inside the success path can break the success path.

The five causes we find most often.

  • Shared try block: the fetch, the success message and the tracking calls all live in one try, so a TypeError from clarity(), gtag() or fbq() lands in the same catch that shows the error UI
  • Global not defined yet: the vendor script is loaded by a tag manager or a deferred script loader, so for a window after page load the global simply does not exist, and calling it throws
  • Snippet never ran: a consent tool withheld the snippet, or an ad blocker stopped the tag manager container that was supposed to inject it, so the global is undefined for that whole visit
  • Tracking before rendering: the code fires conversion events first and updates the UI second, so any exception or slow vendor call delays or cancels the success state
  • Waiting on a vendor callback: the success handler waits for an event_callback or a promise from an analytics library before showing the confirmation, and the callback never fires when the script is blocked

Why does calling clarity, gtag or fbq ever throw?

The official snippets for Clarity, the Google tag and the Meta pixel all define a small queueing function first, so calls made before the full library loads are stored, not lost. That is why a call made after the inline snippet runs normally does not throw, even if the library itself is slow or blocked. The throw happens when the code runs and the snippet has not.

Here is how each vendor defines its global, from the official install code:

  • Microsoft Clarity: the tracking code you paste into the head starts with c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)}, which creates window.clarity as a function that pushes each call onto a q array until the Clarity script loads. The documented API is window.clarity("event", name), window.clarity("set", key, value) and window.clarity("identify", id)
  • Google tag: the snippet sets window.dataLayer = window.dataLayer || []; then function gtag(){dataLayer.push(arguments);}. If the page uses Google Tag Manager instead of this snippet, there is no gtag function on the page unless you define it
  • Meta pixel: the base code creates fbq as a function that calls callMethod once fbevents.js has loaded and otherwise pushes the arguments onto fbq.queue

So the stub protects you only if the stub exists. Clarity can also be installed through Google Tag Manager, using the official template or a Custom HTML tag with the tracking code. In that setup window.clarity does not exist until the container loads and fires the tag, and it never exists for a visitor whose blocker stops the container or who has not accepted the consent category the tag sits in. The same is true for gtag and fbq when they are injected by a tag manager. If your Clarity tag is not appearing at all, our guide to a Google Tag Manager tag not firing covers the container side.

How do you confirm analytics is breaking the form?

Reproduce the submit with an ad blocker on and with the analytics scripts blocked, and watch the console: a TypeError naming clarity, gtag or fbq right before the error message is the proof. Three checks, in this order.

Browser console with request blocking. Open developer tools, go to the Network panel, right-click the vendor request (clarity.ms, googletagmanager.com, connect.facebook.net) and choose Block request URL, then reload and submit a test lead. Chrome and Edge both support this. Then check the Console for an uncaught TypeError such as window.clarity is not a function or gtag is not defined.

Server logs against the UI. Submit once and confirm the server returned a success status in the Network panel (the POST to your form endpoint shows 200 or 201) while the page shows your error state. A successful response plus an error UI means the failure is client side, after the response.

Fast submit after load. Hard reload, fill the form as fast as you can and submit before the page settles. If the error only shows on fast submits, the global was not defined yet, which is the loading window we hit with Clarity.

Diagnose top to bottom.

Confirm the lead was saved.
Check the database, CRM or notification email for the test submission. If it is there and the visitor saw an error, the server is not your problem. Stop looking at the API.
Read the Network response.
Find the POST to your form endpoint in the Network panel. A 2xx status with an error UI on screen means the client code failed after success. A 4xx or 5xx is a real server failure and a different article.
Read the Console.
Look for an uncaught or logged TypeError that names a vendor global. If your catch block swallows the error, temporarily add console.error(err) to it so you can see what it caught.
Find the try block.
Open the submit handler. List every line inside the try after the fetch resolves. Any line that touches window.clarity, gtag, fbq, dataLayer, uetq or a vendor SDK is a suspect.
Block each vendor and resubmit.
Block one vendor host at a time with Block request URL and submit again. The vendor whose block reproduces the error is the one whose call is unguarded.
Move success above tracking and wrap each call.
Set the success state first, then call a track() helper per vendor that checks the global and catches its own errors. Retest with every vendor blocked at once: the form must still show success.

What does a safe tracking call look like?

Every tracking call after a submit should check that the global is a function, run inside its own try/catch, and fire only after the success state is on screen. One helper does it for every vendor:

function safeTrack(name, fn) { try { fn(); } catch (e) { console.warn("tracking failed:", name, e); } }

The submit handler then keeps business logic and measurement in separate blocks:

let res; try { res = await fetch("/api/lead", { method: "POST", body }); } catch (e) { showError(); return; }
if (!res.ok) { showError(); return; }
showSuccess();
setTimeout(() => {
  safeTrack("clarity", () => { if (typeof window.clarity === "function") window.clarity("event", "lead_submit"); });
  safeTrack("gtag", () => { if (typeof window.gtag === "function") window.gtag("event", "generate_lead"); });
  safeTrack("fbq", () => { if (typeof window.fbq === "function") window.fbq("track", "Lead"); });
}, 0);

Three rules make this hold. The try block that decides success covers only the network request and the response check. showSuccess() runs before any vendor code. Each vendor gets its own try/catch, so a broken Clarity call cannot stop the Google Ads conversion from firing. The setTimeout moves tracking to a later task, so the browser can paint the confirmation first.

If you use Google Tag Manager, pushing a custom event to the data layer is safer than calling vendors directly, because window.dataLayer = window.dataLayer || [] always gives you an array to push to: (window.dataLayer = window.dataLayer || []).push({ event: "lead_submit" }). The tags inside the container then handle each vendor. Do not wait on event_callback to show success or to redirect without a timeout fallback, because a blocked script never calls it back.

Form errors caused by analytics: symptom, cause, fix
SymptomLikely causeFix
Lead saved, visitor sees an errorTracking call threw inside the shared try blockSplit the try: request only, then success, then tracking
Error only on fast submits after loadVendor global not defined yet (deferred or tag manager load)Guard with typeof check, fire after success
Error only for some visitorsAd blocker or declined consent kept the snippet from runningGuard every call; never require the global to exist
Duplicate leads minutes apartVisitor retried after a false error messageFix the error path, then dedupe by email and time window
Confirmation shows late or neverSuccess waits on an analytics callback or promiseShow success first; add a timeout to any callback you await
Ad platform conversions far below leadsTracking ran before success and was skipped by the throwFire each vendor in its own try/catch after success
gtag is not defined on a GTM siteSite uses GTM, not the gtag snippetPush a dataLayer event and let GTM fire the tags
One broken vendor stops all othersAll vendor calls share one try/catchOne try/catch per vendor

How many lost conversions are normal?

Some gap between leads in your CRM and conversions in an ad platform is normal, because blocked scripts and declined consent mean some visitors are never measured; visitors seeing an error on a successful submit is never normal. Platforms do not publish a standard acceptable gap, and it varies with audience, consent rules and blocker use, so we do not quote one.

Normal: conversions in GA4, Google Ads or Meta trail CRM leads, the gap is steady week to week, and it narrows when you add server-side signals such as enhanced conversions or the Meta Conversions API. Red flags: duplicate leads from the same person within minutes, support emails saying the form is broken while leads keep arriving, a sudden widening of the gap right after a tracking change, or session recordings that show the error message after a 2xx response. Our conversion tracking audit checklist includes a blocked-scripts submit test for exactly this reason.

As of October 2026, Microsoft's documented Clarity tracking code, the Google tag snippet and the Meta pixel base code all still define a queueing stub before their libraries load. Install methods change, though, so re-check how each global gets onto your page whenever you move a vendor into or out of a tag manager.

Key terms in plain English.

  • Stub: a tiny placeholder function defined by a vendor's inline snippet that stores your calls in a queue until the real library loads
  • Guarded call: a tracking call that first checks the global is a function and catches its own errors, so it can never throw into your code
  • Shared try block: one try/catch wrapping both the business action and the tracking, so a failure in either looks like a failure of the business action

Can Microsoft Clarity break my form?

Clarity itself does not touch your form, but your own code that calls window.clarity can. If that call runs when window.clarity is not a function and sits inside the try block that decides success, the catch block shows an error even though the lead was saved. We hit this while Clarity was still loading. Check the global with typeof and wrap the call in its own try/catch.

Why does my form say error but the submission still goes through?

The request succeeded and something after it threw. The most common culprit is an analytics call (clarity, gtag, fbq) in the same try block as the success message. Check the Network panel for a 2xx response and the Console for a TypeError naming a vendor function.

Do ad blockers break form submissions?

They can, indirectly. When a blocker stops a tag manager container or a consent tool withholds a snippet, the vendor global is never defined, and an unguarded call to it throws. If that call is in the submit's success path, the visitor sees an error. Guard every tracking call so the form works with every script blocked.

Should I check typeof window.gtag before calling it?

Yes. On a page with the Google tag snippet gtag is always a function, but on GTM-only sites, before a deferred load, or when consent withholds the snippet, it does not exist. A typeof check costs nothing and turns a crash into a skipped event.

Should tracking fire before or after the success message?

After. Show the success state first, then fire tracking, ideally in a later task with setTimeout so the browser paints the confirmation. Tracking that fires first can delay or cancel the message the visitor needs to see.

Work with us

Let’s talk about what’s next.

A short note on where the business is and where it needs to go. A senior partner replies within one business day.

t@theoryroad.com

Step 1 of 2

How can we help you get found?

What do you need help with?

Next: name, email and budget. That’s it.