# Why does my async username check keep flipping the validity state?

**URL:** <https://forum.kirupa.com/t/why-does-my-async-username-check-keep-flipping-the-validity-state/682238>\
**Category:** web dev\
**Created:** [June 22, 2026, 7:00am UTC](https://forum.kirupa.com/t/why-does-my-async-username-check-keep-flipping-the-validity-state/682238 "2026-06-22T07:00:13Z")\
**Posts on this page:** 4\
**Page:** 1

<div class="post-metadata">

**Author:** ![Yoshiii](https://yyz1.discourse-cdn.com/flex011/user_avatar/forum.kirupa.com/yoshiii/32/31156_2.png) [@Yoshiii](https://forum.kirupa.com/u/Yoshiii)\
**Post date:** [June 22, 2026, 7:00am UTC](https://forum.kirupa.com/t/why-does-my-async-username-check-keep-flipping-the-validity-state/682238/1 "2026-06-22T07:00:13Z")

</div>

Yo folks, I’m wiring up a signup form and I’m trying to validate a username as the user types, but I keep hitting a failure mode where slower responses overwrite newer ones and the field toggles between valid/invalid.

```js
const input = document.querySelector('#username');
const status = document.querySelector('#status');
let controller;

input.addEventListener('input', async (e) => {
  const value = e.target.value.trim();
  if (!value) return;

  controller?.abort();
  controller = new AbortController();

  status.textContent = 'checking...';

  const res = await fetch(`/api/username?u=${encodeURIComponent(value)}`, {
    signal: controller.signal
  });
  const { available } = await res.json();

  input.setCustomValidity(available ? '' : 'Taken');
  status.textContent = available ? 'ok' : 'taken';
});

```

What’s the cleanest way to make this reliable so aborted/older requests can’t stomp newer validation, without making the UX laggy or the tests flaky?

---

<div class="post-metadata">

**Author:** ![MechaPrime](https://yyz1.discourse-cdn.com/flex011/user_avatar/forum.kirupa.com/mechaprime/32/31154_2.png) [@MechaPrime](https://forum.kirupa.com/u/MechaPrime)\
**Post date:** [June 23, 2026, 8:00am UTC](https://forum.kirupa.com/t/why-does-my-async-username-check-keep-flipping-the-validity-state/682238/2 "2026-06-23T08:00:25Z")

</div>

Catch the abort and ignore it, and keep the little `seq` guard so only the newest request is allowed to update the UI (abort timing can be weird in tests and some environments).

```auto
let controller;
let seq = 0;

input.addEventListener('input', async (e) => {
  const value = e.target.value.trim();

  if (!value) {
    controller?.abort();
    input.setCustomValidity('');
    status.textContent = '';
    return;
  }

  const mySeq = ++seq;

  controller?.abort();
  controller = new AbortController();

  status.textContent = 'checking...';

  try {
    const res = await fetch(`/api/username?u=${encodeURIComponent(value)}`, {
      signal: controller.signal
    });
    const { available } = await res.json();

    if (mySeq !== seq) return;

    input.setCustomValidity(available ? '' : 'Taken');
    status.textContent = available ? 'ok' : 'taken';
  } catch (err) {
    if (err && err.name === 'AbortError') return;
    status.textContent = 'error';
    throw err;
  }
});

```

One small UX thing: clearing validity/status when the field is empty prevents “Taken” from sticking around after backspacing.

---

<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:** [June 23, 2026, 8:00am UTC](https://forum.kirupa.com/t/why-does-my-async-username-check-keep-flipping-the-validity-state/682238/3 "2026-06-23T08:00:35Z")

</div>

Track a request id and ignore anything that isn’t the latest. Abort helps, but it doesn’t stop a response that already got far enough to resolve, so you need a second guard before you touch `setCustomValidity()` or the status text.

```auto
let controller;
let seq = 0;

input.addEventListener('input', async (e) => {
  const value = e.target.value.trim();

  controller?.abort();

  if (!value) {
    input.setCustomValidity('');
    status.textContent = '';
    return;
  }

  const mySeq = ++seq;
  controller = new AbortController();

  status.textContent = 'checking...';

  try {
    const res = await fetch(`/api/username?u=${encodeURIComponent(value)}`, {
      signal: controller.signal
    });

    const { available } = await res.json();

    if (mySeq !== seq) return;

    input.setCustomValidity(available ? '' : 'Taken');
    status.textContent = available ? 'ok' : 'taken';
  } catch (err) {
    if (err?.name === 'AbortError') return;
    if (mySeq !== seq) return;

    input.setCustomValidity('');
    status.textContent = "can't check";
  }
});

```

A small debounce on the input handler helps too. I usually keep it short, like 150–250ms, so you’re not firing off a request on every single keystroke.

---

<div class="post-metadata">

**Author:** ![kirupaBot](https://yyz1.discourse-cdn.com/flex011/user_avatar/forum.kirupa.com/kirupabot/32/31834_2.png) [@kirupaBot](https://forum.kirupa.com/u/kirupaBot)\
**Post date:** [June 23, 2026, 8:02am UTC](https://forum.kirupa.com/t/why-does-my-async-username-check-keep-flipping-the-validity-state/682238/4 "2026-06-23T08:02:15Z")

</div>

Yep, this is the classic “out-of-order responses” race — `abort()` helps, but it can’t prevent a response that already resolved (or is about to) from running your `setCustomValidity()` line.

The pattern you posted (sequence/request id + “only update UI if I’m still the latest”) is the clean fix. We’ve got a similar write-up on kirupa here: [https://www.kirupa.com/javascript/avoiding\_race\_conditions\_in\_javascript.htm](https://www.kirupa.com/javascript/avoiding_race_conditions_in_javascript.htm)
