Fixes · Cart scripts

Cart quantity resets to 1, or quick clicks leave the wrong items in the cart

The fix

/cart/change.js treats a missing quantity as 1, replaces a line’s properties rather than merging them, and won’t clear them with an empty object. Send the line’s quantity every time, send the full set of properties, and blank a property out to clear it (the key stays with an empty value; Shopify’s own advice for removing them all is to remove the line).

Run cart changes one at a time, and build each one from the cart as it is when its turn comes, not as it was when the shopper tapped.

01The cart API rules that catch people out

Shopify’s cart API does what its reference says, but some of the rules sit where you wouldn’t look (Shopify, Cart API reference). On this Shopify Community thread (Nov 2023), a developer’s cart/change.js “returns 200 but the properties are not updated, seemingly ignored” (the poster suspected a bundles app), and no fix was posted. Four things are worth knowing:

  1. 01

    A missing quantity means 1

    The reference’s own example for updating properties sends a line and its properties with no quantity. The warning sits in a note about selling plans: “If the quantity isn’t specified, then it’s assumed to be 1.” On a store we are building, saving a detail onto a cart line of 2 with no quantity took it down to 1.

  2. 02

    Properties are replaced, and can’t be emptied

    “Whenever a POST request includes properties, it overwrites the entire properties object.” A script that saves one property wipes the others unless it sends them all. And “It’s not possible to set a line item’s properties property to an empty object once a value is set”: the request changes nothing, and a script that retries until the line matches can loop.

  3. 03

    Line keys change

    “The line item key is not persistent for the lifetime of a line item, it changes as characteristics of the line item change. This includes, but is not limited to, properties and discount applications.” On this developer forum thread (May 2026), Shopify’s reply to random 400 errors made the same point: “a key that was valid when the cart drawer rendered may not always still be valid by the time the remove request is made.” (That poster’s errors turned out to have another cause, but the point about keys stands.)

  4. 04

    Overlapping requests each act on the cart they saw

    Shopify now runs simultaneous cart requests instead of sometimes dropping one, and may answer 409 when there are too many (Shopify changelog, 21 Oct 2025). That stopped lost requests, not stale ones. Two corrections worked out from the same cart both run, and the second names a line the first has removed. On the store where we saw it, Shopify refused those with a 422. Before that change, developers reported overlapping calls where “Both calls come back with ok status, but one of them ‘wins’.”

02One change at a time, from the cart as it is

For developers: one queue, fresh reads, full writes

This is the shape of what we shipped on two stores, generalised. Every cart change goes through one queue, and each one reads the cart when its turn comes:

A queue for cart changes, and a property save that keeps the quantityjs
// One cart change at a time, in order. A failed change doesn't stop the next.
let tail = Promise.resolve();
function queueCartChange(task) {
  const run = tail.then(task, task);
  tail = run.catch(() => {});
  return run;
}

async function getCart() {
  const res = await fetch(window.Shopify.routes.root + 'cart.js');
  return res.json();
}

async function postCart(path, body) {
  const res = await fetch(window.Shopify.routes.root + path, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
    body: JSON.stringify(body),
  });
  if (!res.ok) throw new Error(path + ' ' + res.status);
  return res.json();
}

// Save properties onto a line. Find the line by something that doesn't
// change (its variant, or a property you set), not by an old key.
function setLineProperties(matchLine, changes) {
  return queueCartChange(async () => {
    const cart = await getCart(); // read now, not when the shopper tapped
    const line = cart.items.find(matchLine);
    if (!line) return cart; // the line has gone: nothing to do

    return postCart('cart/change.js', {
      id: line.key,
      quantity: line.quantity, // a missing quantity is read as 1
      properties: { ...(line.properties || {}), ...changes }, // replaces the whole set
    });
  });
}

// Removing properties: blank them. An empty object changes nothing.
function clearLineProperties(matchLine, names) {
  const blanks = {};
  names.forEach((name) => { blanks[name] = ''; });
  return setLineProperties(matchLine, blanks);
}
  • Send quantity with every /cart/change.js, read from the cart right before the write, never from the page. The number on the page can be a change behind.
  • Your queue only orders your own requests. The theme’s cart and apps change the cart too, which is why each change reads the cart when its turn comes. If the theme shows when it’s busy, wait for that as well, as we did on Carnoisseur.
  • Keep the line’s other properties: spread its current properties in, then your changes.
  • Blank properties stay on the line with an empty value. Dawn’s cart skips them (property.last != blank), and so do many themes, but check your cart and your order pages before you rely on it.
  • If your cart corrects itself (a cap on free samples, a gift that must go with a paid item), queue one correction at a time and work out what to correct when it runs. A correction worked out when it was queued can be out of date by then.
  • If the cart applies corrections, make the Checkout button wait for any change in flight, apply the correction itself, then submit the form. Don’t send the correction inside the checkout form while another request is still changing the cart.
  • On a form that has an input named id (every product form does), form.id returns that input, not the form’s id. Use form.getAttribute('id').

03Tap fast, on a slow connection, in two tabs

  1. Save a detail on a line of 2

    Set a line to quantity 2, then do whatever writes the property (a gift note, an engraving, a confirm box). The line should stay at 2.

  2. Clear it

    Empty the field and save. The detail should be gone from the cart, and the page shouldn’t keep saving.

  3. Tap fast on a slow connection

    In Chrome’s developer tools, set the network to Slow 4G and tap the cart’s controls several times quickly. Once the requests finish, the cart should be what your rules allow, with no 400, 409 or 422 in the Network tab.

  4. Change the cart in another tab, then press Checkout

    Open the store in a second tab, change the cart there, go back and press Checkout without reloading. Checkout should show the cart as it is now, with your rules applied.

From our work

04Carnoisseur’s registration box, and a sample limit

Carnoisseur sells spare wheel kits, and on the store we are building for them, the cart asks for the car’s registration so each kit can be matched to the vehicle. A box in the cart looks the registration up and saves the result onto the kit’s line as properties.

Quantity (9 Oct 2026). In testing, saving a registration onto a kit with a quantity of 2 dropped it to 1. Our plan had been to send properties only. Each save now reads the cart right before writing, sends the line’s quantity with the properties, and first waits for any change the theme’s own cart is making.

Clearing (9 Oct 2026). Clearing every field left the old details on the line, because Shopify ignored the empty set, and the save kept trying again. Each line is now written at most once per save, and clearing names each removed property with a blank value.

On a wellness brand’s new theme, the cart gives free samples with paid items, up to two, and corrects the cart when it breaks that rule. A stress test on the theme’s preview (1 Oct 2026) found three ways quick or stale changes beat it:

  • Four sample ticks tapped before the first answer came back left three samples in the cart. Each correction had been worked out when it was queued, so the second named a line the first had removed, Shopify answered 422, and the retry limit stopped any more. Now the correction is worked out when its turn comes, only one waits at a time, and each tap resets the limit.
  • Pressing Checkout while a correction was on its way sent the same correction twice: once as its own request and once inside the checkout form. Whichever landed second named a line that had gone, Shopify answered 422, and the shopper was put back on the cart. Now Checkout makes the correction itself, in its turn, then submits.
  • A cart changed in another tab went to checkout over the limit, because the Checkout form was the one drawn before the cart changed, with no corrections for the cart as it now was. Checkout now waits for any change in flight, redraws from the live cart, corrects it, then submits.

The new Checkout code briefly broke the sample ticks: it read form.id, which on a sample form is its input named id. We caught it on the preview and fixed it within half an hour.

  • In our log, written up“Saving a car's registration in the cart no longer drops a kit's quantity to 1”

The bigger picture

05Carts with rules

Most cart code is tested by clicking slowly on a fast connection. If your cart has rules (free gifts, sample limits, bundles, saved details), test it the way shoppers use it: quickly, on a slow phone connection, and in two tabs.

Carts with rules like these are what our bundles and builders work covers.

06Sources

Every step and claim above comes from one of these, or from the fix on the store it describes.

  1. Shopify developers: Cart API reference
  2. Shopify developer changelog: Improved concurrency handling in the Cart AJAX API (21 Oct 2025)
  3. Shopify Dawn theme: sections/main-cart-items.liquid
  4. Shopify Community: Updating line item property from cart page with AJAX API not working (Nov 2023)
  5. Shopify developer community: Random 400 errors on line item removal (May 2026)
  6. Shopify developer community: Potential race conditions with cart update ajax call (Aug 2025)

Sources checked on .

Contact

A cart with rules that won’t hold?

Tell us the rules (gifts, samples, bundles, saved details) and how shoppers manage to break them.