JavaScript Error Monitoring

"$ is not defined": Fix the jQuery ReferenceError

The $ is not defined error means jQuery loaded late, failed, or got scoped away by noConflict. Diagnose script order, defer/async, and bundler conflicts.

If you've been writing JavaScript for more than a few years, you've seen it: "$ is not defined" (or jQuery is not defined). The error hits different browsers at different times, and it's almost never about jQuery itself — it's about when jQuery loaded relative to your code. This guide walks through every common cause and how to fix it, so your jQuery plugins, selectors, and handlers actually run.

The single-line cause is simple: your code tried to use $ before jQuery finished loading. But "before it loaded" can mean six different things, each with a different fix. In production, these become uncaught ReferenceError exceptions that error tracking will catch, showing you the exact timing and context.

The diagnostic flow: Is jQuery even loaded?

Before you fix anything, answer this one question in the browser console:

typeof jQuery
  • If it returns undefined: jQuery never loaded. Check the Network tab for 404s, blocked CDNs, or offline conditions. (Skip to "jQuery failed to load" below.)
  • If it returns function: jQuery exists, but $ isn't available. This is a scope, timing, or conflict issue. (This is the more common case.)

This distinction cuts the problem space in half. Most developers expect jQuery to be loaded but don't realize they're using it in the wrong scope or at the wrong time.

Cause 1: Script order — jQuery tag is after your script

The oldest cause is the simplest: your <script> tag runs before jQuery's <script> tag loads.

Incorrect order:

<script src="main.js"></script> <!-- Uses $ here -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <!-- Loads jQuery here -->

When main.js runs, jQuery and $ don't exist yet. The fix is to reverse the order:

<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="main.js"></script>

This is bulletproof if you're using plain <script> tags with no async or defer. Modern apps almost never use plain tags alone, though — so keep reading.

Cause 2: jQuery failed to load

jQuery might exist in your HTML, but never load from the CDN. This happens when:

  • The CDN is blocked by CORS (unlikely for jQuery's public CDN, but possible in corporate networks).
  • Network issue or timeout (user is offline, CDN is down, or request took too long).
  • Ad blocker or content blocker (common on client machines).
  • ISP or corporate proxy filtering.

To diagnose: open the Network tab in your browser's dev tools, reload the page, and look for the jQuery request. If it's red (failed) or missing entirely, that's your issue. CDN failures due to CORS or network problems are covered in detail in script-error CORS.

A fallback pattern for production:

<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
  if (typeof jQuery === 'undefined') {
    // CDN failed; load from a backup or local copy
    var script = document.createElement('script');
    script.src = '/js/jquery-3.6.0.min.js'; // Your local copy
    document.head.appendChild(script);
  }
</script>
<script src="main.js"></script>

Wait for that fallback script to load before running main.js, or wrap main.js in <script defer> so the browser defers execution until all prior scripts finish. (See Cause 3 below.)

For production apps, always host a local fallback copy of jQuery. A CDN outage or network blip shouldn't break your page.

Cause 3: async or defer on jQuery breaks order

The modern culprit: the async attribute on your jQuery <script> tag. async means "download in parallel and run as soon as it's ready" — not in the order you wrote it.

<script async src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="main.js"></script> <!-- May run before jQuery loads! -->

Even though jQuery is written first, main.js might run while jQuery is still downloading. async is only safe for scripts that don't depend on others (analytics, tracking pixels, etc.).

Use defer instead. It downloads the script in parallel but executes in the order you wrote it, after the page finishes parsing:

<script defer src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script defer src="main.js"></script> <!-- Guaranteed to run after jQuery -->

Both scripts download in parallel, but they always execute in order. This is the modern sweet spot. Script-loading order is a common source of browser JavaScript errors, especially across different connection speeds and geographies.

Never use async on a dependency. async is only safe for independent code. If your script uses jQuery, DOM elements, or other libraries, use defer instead.

Cause 4: $.noConflict() — another library took $

If another library (Prototype, Underscore, older MooTools, or even a custom framework) loads after jQuery, it can claim the $ global. jQuery is still loaded as jQuery, but $ is shadowed. This is a flavor of the broader "undefined is not a function" problem, where a variable exists but isn't what you expected.

Check the console:

typeof $       // undefined or some other function
typeof jQuery  // function (jQuery is still there)

Two fixes:

Option A: Use jQuery instead of $ in your code:

jQuery('#myId').text('Hello'); // Always works

Option B: Wrap your code in an IIFE to restore $ locally:

(function($) {
  // Inside this function, $ is jQuery
  $('#myId').text('Hello');
})(jQuery);

The IIFE pattern passes jQuery as the parameter named $, so your code uses the expected short alias without polluting the global scope.

Cause 5: Webpack, Vite, or ESM modules — jQuery is not global

With modern bundlers, import $ from 'jquery' creates a module-local variable, not a global $. If you import jQuery in one file and use it in an inline <script> tag in HTML, that HTML script can't see the module's $:

// app.js (bundled by Webpack)
import $ from 'jquery';
$('#button').on('click', () => alert('clicked'));

// index.html
<script>
  $('#button').on('click', () => alert('other handler')); // ReferenceError: $ is not defined
</script>

The bundled app.js runs in its own module scope; the HTML <script> tag is global scope. They can't see each other.

Fix: Attach jQuery to the window object so it's globally accessible:

// app.js
import $ from 'jquery';
window.$ = $;     // Make it global
window.jQuery = $;

Or use Webpack's ProvidePlugin to inject $ globally:

// webpack.config.js
new webpack.ProvidePlugin({
  $: 'jquery',
  jQuery: 'jquery'
})

This tells Webpack to treat $ as a global and automatically inject the import. But be aware: making jQuery global is usually a code smell. It means you're mixing bundled and inline code, which is a maintenance footgun. If you're using a modern bundler, keep jQuery in the module system and avoid global <script> tags altogether.

Cause 6: WordPress loads jQuery in noConflict mode

WordPress bundles jQuery but loads it with jQuery.noConflict(), so the global $ is not jQuery. Inside WordPress, you must use jQuery or the callback pattern:

// Doesn't work in WordPress
$('#post').hide(); // ReferenceError

// Correct in WordPress
jQuery('#post').hide(); // Works

// Or use the callback pattern
jQuery(function($) {
  // Inside this callback, $ is safely jQuery
  $('#post').hide();
});

If you're writing a WordPress plugin or theme, always use the callback pattern or jQuery directly. This is by design — WordPress protects you from conflicts with other jQuery versions or libraries that might also need $.

WordPress's noConflict mode is not a bug; it's a feature. Your plugin might coexist with dozens of other plugins, and WordPress ensures $ doesn't get tangled. Embrace the callback pattern.

Cause 7: Code runs before DOM and scripts are ready

Even when jQuery loads, code that runs immediately might use selectors that don't find anything, or fire event handlers before the page is interactive.

<script src="jquery.js"></script>
<script>
  $('#submit-btn').on('click', handler); // Button doesn't exist yet; selector finds nothing
</script>
<!-- ... later in HTML ... -->
<button id="submit-btn">Submit</button>

The script runs before the button exists in the DOM. Use $(document).ready() to defer until the page loads:

$(document).ready(function() {
  $('#submit-btn').on('click', handler); // Button exists now
});

Or the shorthand:

$(function() {
  $('#submit-btn').on('click', handler); // Same as $(document).ready(...)
});

Catching it in production

These errors are uncaught ReferenceError exceptions — they bubble to the global error handler and stop your code cold. In production, this is invisible unless you're watching. Error tracking captures the exact moment $ is not defined, the stack frame, and the user's environment, so you see whether it's a timing issue (mostly old browsers) or a global conflict (one user with a conflicting extension).

When you see a spike in "$ is not defined" errors after a deploy, a global error handler gives you the breadcrumb trail: Did you add async to a script tag? Did a third-party library load in a new order? With the right monitoring, the fix becomes obvious. For a structured approach to production investigation, see how to debug production errors.

Start tracking errors in minutes

Stop flying blind with jQuery errors. LightTrace captures the full stack, user context, and breadcrumb trail so you can fix scope and loading issues before they affect your users.

Fix your next production error faster

Point any Sentry SDK at LightTrace — free up to 5,000 events/month.