Webclat / Tealium Practice

Why do I get "utag is not defined," including cases where it only happens in private/incognito browsing?

Answer

"utag is not defined" means code ran that referenced the global utag object before Tealium's tag.js file finished loading and initializing it - either the reference fired too early in page load, or the tag.js request itself never completed. Private/incognito browsing usually adds a third cause: an ad-blocker or tracking-protection list bundled into private mode silently blocks the tag.js request, so the library never loads at all in that mode even though the exact same page works normally otherwise.

Why this happens

Tealium's utag.js loads asynchronously by design so it doesn't block page render - anything that calls utag.* before that script parses and executes finds no such global yet.

  • Common triggers: an inline onclick handler that calls utag.link() directly, a script tag placed above the Tealium loader snippet, a framework route change firing before hydration completes, or a tag inside Tealium itself trying to call back into utag before iQ's own bootstrap finishes.
  • Private/incognito-specific cause: Safari ITP, Firefox Enhanced Tracking Protection, and browser-bundled ad-block lists are frequently stricter in private windows, and tag.js served from a CDN can get pattern-matched as a "tracker" and blocked outright. That isn't a timing race - the file never arrives at all.

Fix it

  1. Confirm the Tealium loader snippet sits in <head> and is the standard async loader, not a hand-edited synchronous version with a stray defer/async attribute removed.
  2. Never call utag.* directly from an inline HTML attribute. Route the call through a wrapper that checks typeof utag !== "undefined" && typeof utag.link === "function" before calling, and no-ops otherwise.
  3. For calls that must fire early (first-interaction tracking), queue them instead: push an object describing the event to a plain array (window.utagq = window.utagq || []; window.utagq.push({...})) and drain that queue from a Tealium extension or a loader callback once the library is confirmed ready - don't call utag.link directly from application code at that point.
  4. For SPA route-change tracking, hook the call to the same lifecycle event your app already uses for routing, not a fixed setTimeout - and still guard it with the typeof check above, since a route change can complete before utag.js has loaded.
  5. For the private-mode-only case specifically, reproduce in an actual private/incognito window (not just "disable extensions"), open the Network tab, and check whether the tag.js request itself appears and succeeds. If it's blocked or missing, this isn't a code bug - the fix is moving the library behind a first-party path (server-side or first-party proxying) or accepting that segment isn't measurable client-side.

How to verify it worked

  • Open DevTools Console and run typeof utag at every point in the page lifecycle where your code calls it - it should read "object", never "undefined".
  • Open the Network tab, filter by "utag", and confirm the tag.js (and utag.sync.js if used) requests return 200, not blocked or cancelled - a red "blocked" entry there is the private-mode signature.
  • Reproduce in Tealium's own Live/Trace mode alongside a real private-browsing session side by side: a standard window should show the event in trace; a private window either shows it too (fixed) or shows the request never leaving the browser (confirms the tracking-protection cause, not a code cause).
  • Add a one-line console.warn inside your guarded wrapper's "not ready" branch during testing, so a still-broken setup shows visible evidence in the console instead of a silent no-op that looks identical to success.

One check before you assume it's a bug: confirm the error is reproducible on the live page over a real network request, not just a local/staging build with a different (or missing) loader snippet - a surprising share of "utag is not defined" reports trace back to a staging environment that never had the snippet added at all.

Get a runtime map of what's actually loading.

We trace the live page - not the snippet in your CMS editor - and tell you plainly whether this is a timing bug or a blocked request.

Audit My Tealium Load