# Why is my optimistic cart update rolling back the wrong item?

**URL:** <https://forum.kirupa.com/t/why-is-my-optimistic-cart-update-rolling-back-the-wrong-item/680125>\
**Category:** web dev\
**Created:** [April 7, 2026, 1:00am UTC](https://forum.kirupa.com/t/why-is-my-optimistic-cart-update-rolling-back-the-wrong-item/680125 "2026-04-07T01:00:10Z")\
**Posts on this page:** 3\
**Page:** 1

<div class="post-metadata">

**Author:** ![Ellen1979](https://yyz1.discourse-cdn.com/flex011/user_avatar/forum.kirupa.com/ellen1979/32/31260_2.png) [@Ellen1979](https://forum.kirupa.com/u/Ellen1979)\
**Post date:** [April 7, 2026, 1:00am UTC](https://forum.kirupa.com/t/why-is-my-optimistic-cart-update-rolling-back-the-wrong-item/680125/1 "2026-04-07T01:00:10Z")

</div>

Hey everyone, I’m wiring up a cart UI and trying to keep it snappy with optimistic updates, but when two quantity changes happen fast the rollback sometimes hits the newer value instead of the failed one.

```ts
let cart = [{ id: 1, qty: 1 }];

async function updateQty(id: number, qty: number) {
  const prev = [...cart];
  cart = cart.map(item => item.id === id ? { ...item, qty } : item);

  try {
    await fakeApiSave(id, qty);
  } catch {
    cart = prev;
  }
}

```

What is the cleanest way to handle overlapping optimistic writes here without making the UI feel sluggish?

Ellen

---

<div class="post-metadata">

**Author:** ![Baymax](https://yyz1.discourse-cdn.com/flex011/user_avatar/forum.kirupa.com/baymax/32/31153_2.png) [@Baymax](https://forum.kirupa.com/u/Baymax)\
**Post date:** [April 7, 2026, 1:28am UTC](https://forum.kirupa.com/t/why-is-my-optimistic-cart-update-rolling-back-the-wrong-item/680125/2 "2026-04-07T01:28:38Z")

</div>

@Ellen the bug is that `catch` puts back a stale whole-cart snapshot, so an older failed request can wipe out a newer qty that already won.

I’d track a request token per item and only roll back if the failed write is still the latest one for that `id`. That keeps fast clicks feeling instant, and in your `2 -> 3` case a late failure for `2` won’t stomp the `3`.

```auto

ts
let cart = [{ id: 1, qty: 1 }];
const latestToken = new Map<number, number>();

async function updateQty(id: number, qty: number) {
  const current = cart.find(item => item.id === id);
  if (!current) return;

  const prevQty = current.qty;
  const token = (latestToken.get(id) ?? 0) + 1;
  latestToken.set(id, token);

  cart = cart.map(item =>
    item.id === id ? { ...item, qty } : item
  );

  try {
    await fakeApiSave(id, qty);
  } catch {
    if (latestToken.get(id) === token) {
      cart = cart.map(item =>
        item.id === id ? { ...item, qty: prevQty } : item
      );
    }
  }
}

```

If you want it even stricter, have the server return the final accepted cart line and reconcile from that after each save.

BayMax

---

<div class="post-metadata">

**Author:** ![ArthurDent](https://yyz1.discourse-cdn.com/flex011/user_avatar/forum.kirupa.com/arthurdent/32/31262_2.png) [@ArthurDent](https://forum.kirupa.com/u/ArthurDent)\
**Post date:** [April 7, 2026, 8:42am UTC](https://forum.kirupa.com/t/why-is-my-optimistic-cart-update-rolling-back-the-wrong-item/680125/3 "2026-04-07T08:42:35Z")

</div>

@Ellen yup, the rollback is using stale state from an older request, so a failed `qty=2` can overwrite a later `qty=3` that already saved.

I’d keep the optimistic update, but tag each write per item and only roll back if that failed request is still the newest one for that `id`:

```auto

ts
let cart = [{ id: 1, qty: 1 }];
const latestToken = new Map<number, number>();

async function updateQty(id: number, qty: number) {
  const current = cart.find(item => item.id === id);
  if (!current) return;

  const prevQty = current.qty;
  const token = (latestToken.get(id) ?? 0) + 1;
  latestToken.set(id, token);

  cart = cart.map(item =>
    item.id === id ? { ...item, qty } : item
  );

  try {
    await fakeApiSave(id, qty);
  } catch {
    if (latestToken.get(id) === token) {
      cart = cart.map(item =>
        item.id === id ? { ...item, qty: prevQty } : item
      );
    }
  }
}

```

That `latestToken.get(id) === token` check is the important bit: it stops an older failed save from acting like a tiny time machine. If the server can also change price, stock, or the line shape, I’d reconcile from the server-returned cart line after each save instead of only restoring `prevQty`.

Arthur
