Available — Local Digital Products
HCA · Studio
TR Contact ↗
Technical Note 02 · Debugging

"Your cart is empty" and deferred JavaScript

Products go into the cart, the counter increments — and checkout says the cart is empty. The most common cause is not caching but a speed plugin deferring the JavaScript block checkout depends on.

Symptom
Block checkout shows an empty cart
Root cause
Blanket JS defer
Time
15-minute fix
Short answer

WooCommerce's block-based cart and checkout depend on WordPress's wp JavaScript object and the wp-* script packages. When a speed plugin or custom code defers every script indiscriminately, those packages execute in the wrong order, the console throws wp is not defined, and the block renders its empty-cart state. The fix is to exempt WordPress core and WooCommerce block scripts from the defer rule — not to clear the cart or the cache.

Symptoms

This failure has a distinctive signature:

  • "Add to cart" works on the product page and the header cart counter increments.
  • The cart page appears empty, or does not render at all.
  • Checkout says "Your cart is currently empty".
  • The problem is absent for some visitors — typically it never appears for a logged-in administrator.
  • There is nothing in the PHP error log.

The last two matter. It does not appear for an administrator because most speed plugins disable optimisation for logged-in users. That delays the site owner noticing anything: the failure only happens to real customers.

Wrong diagnoses

Most people try these in order, and none of them fixes it permanently:

  • Clearing the cache. It can look fixed because the first load changes script timing; minutes later it returns.
  • Excluding checkout from cache. A correct setting that should be applied anyway — but not the cause here.
  • Changing session or cookie settings. Done on the assumption that the cart is a session problem; but the cart is correct on the server, the problem is on screen.
  • Reinstalling the theme or WooCommerce. Wasted time.

The shared error is looking for the problem on the server. The server knows the cart perfectly well — the incrementing counter proves it.

The right diagnosis: the console

Open checkout while logged out (a private window) and look at the browser console.

Uncaught ReferenceError: wp is not defined
    at checkout-blocks.js:1
Uncaught TypeError: Cannot read properties of undefined (reading 'createElement')

If you see those two lines, the diagnosis is complete. The cart is fine, the server is fine; the JavaScript that draws the page dies before it can run.

Extra confirmation: look at the <script> tags in page source. If WordPress core scripts (wp-polyfill, wp-element, wp-data) also carry defer, the cause is certain.

Root cause

WordPress scripts depend on each other, and that order is declared through wp_enqueue_script. Normally the browser preserves it.

Adding defer makes scripts run after HTML parsing. The specification says deferred scripts run in document order — but with dynamically added scripts, conditionally loaded bundles and inline code mixed in, that guarantee breaks in practice. In particular:

  • Inline scripts cannot be deferred. WooCommerce blocks pass their settings through inline <script> blocks. That inline code runs immediately, while the wp-element it depends on has been deferred. The order inverts.
  • Modules and classic scripts mix. Forcing different loading models through one defer rule makes timing unpredictable.

The result: code trying to reach wp runs before it exists, throws, and the React-based block never mounts. Unmounted, it shows its default state — an empty cart.

When I traced this to root cause on a real site, the source was a blanket defer rule in a helper plugin attached to the theme: a few lines adding defer to every script on the site, with no exception list.

The fix

You do not remove the defer rule — you give it an exemption list. WordPress core and WooCommerce block scripts must not be deferred.

add_filter( 'script_loader_tag', function ( $tag, $handle ) {

    // Handles that must never be deferred
    $skip_prefixes = array( 'wp-', 'wc-', 'woocommerce' );
    $skip_exact    = array( 'jquery', 'jquery-core', 'jquery-migrate' );

    foreach ( $skip_prefixes as $prefix ) {
        if ( 0 === strpos( $handle, $prefix ) ) {
            return $tag;
        }
    }
    if ( in_array( $handle, $skip_exact, true ) ) {
        return $tag;
    }

    // Never defer anything on cart and checkout
    if ( function_exists( 'is_checkout' ) && ( is_checkout() || is_cart() ) ) {
        return $tag;
    }

    if ( false !== strpos( $tag, ' defer' ) || false === strpos( $tag, ' src=' ) ) {
        return $tag;
    }

    return str_replace( ' src=', ' defer src=', $tag );

}, 10, 2 );

With an off-the-shelf speed plugin the same logic goes through its settings: the JavaScript defer/combine section has an exclusion list. Add handles starting with wp-, wc- and woocommerce, and where possible exempt cart and checkout from optimisation entirely.

Verification

  1. In a private window (logged out), add a product to the cart.
  2. Open checkout; the cart should be populated.
  3. The console should be clean — wp is not defined in particular must be gone.
  4. Check that the payment method list is complete. The same failure also makes custom gateways vanish from block checkout.
  5. Clear the cache and repeat — the fix must hold on the first cached visit too.

Summary

"Your cart is empty" is usually not a cart bug but a script loading order bug. The server knows the cart; the JavaScript that would draw it cannot run.

The general rule: speed optimisation is never applied without exceptions. Blanket defer, combine or delay rules must exclude WordPress core scripts and the payment flow. Speed should not be paid for in sales.

Quick diagnosis
Symptom
Cart counter increments but checkout shows an empty cart
Key clue
No problem when logged in as admin; fails in a private window
Console error
Uncaught ReferenceError: wp is not defined
Root cause
defer added to every script with no exceptions
Fix
Exempt wp-, wc- and woocommerce handles from deferral
Extra guard
Disable optimisation entirely on cart and checkout
Related symptom
A custom payment gateway never appears in block checkout
Frequently Asked Questions

About this error.

Products add to the cart but checkout says it's empty — why?

The server almost certainly knows the cart correctly, but the JavaScript that renders it cannot run. Block-based cart and checkout depend on WordPress's wp object; when a speed plugin defers every script, that object is not defined in time and the block renders itself as an empty cart.

Why does it only affect logged-out visitors?

Because most speed plugins disable caching and script optimisation for logged-in users. Browsing as an administrator, no optimisation is applied and no error occurs. This is why testing must always be done in a private window, logged out.

Is clearing the cache the fix?

No. It can look fixed because the first load changes script timing, but the problem returns shortly after. The permanent fix is adding an exclusion list to the defer rule covering WordPress core and WooCommerce scripts.

Should I turn off JavaScript deferral entirely for speed?

No need. Deferral gives a real gain on pages outside the payment flow. What matters is not applying it without exceptions: exempt scripts starting with wp-, wc- and woocommerce, and disable optimisation entirely on cart and checkout.

Availability and Quotes

Visible in local search.
Credible in the product.