A field's rule must ask a server whether a value is already taken; what does that asynchronous rule need beyond a synchronous one?
answer
- a verdict about a value already gone
- three states, not two
- debounce, cancel, match the reply
- the pass has a shelf life
basics
~20 sIt needs a debounce, cancellation of superseded requests, a pending state that is neither valid nor invalid, a check that the reply still matches the current value, and a submit path that accounts for it.
solid answer
~50 sAn asynchronous rule returns a verdict about a value that may already be gone, and that gap creates five obligations. **Debounce**: wait for a pause in typing, so one request per intent rather than one per keystroke. **Cancellation**: abort or ignore the in-flight request when a newer value arrives, otherwise replies can land out of order and an older verdict overwrites a newer one. **A pending state**: while the check runs the field is neither valid nor invalid, and the interface should say so instead of guessing. **Value matching**: discard a reply whose value no longer matches the field's. **A submit rule**: submit must either await the in-flight check or treat the result as advisory, because the server re-checks anyway and can reject a value the client last saw as free. A failed request is also not a failing value - surface it as a retryable problem, not as taken.
code
pseudocode · 25 linesstate field.value = ""
state field.status = "idle" # idle | pending | pass | fail | errored
state inFlight = none
state cache = empty map
on change(newValue):
field.value = newValue
field.status = "idle"
if not localRulesPass(newValue): return # do not spend a round trip
if cache has newValue:
field.status = cache[newValue]
return
debounce 350ms then runCheck(newValue)
runCheck(candidate):
cancel inFlight if any
field.status = "pending"
inFlight = ask server "is candidate taken?"
on reply(result):
if candidate != field.value: return # stale: the value moved on
cache[candidate] = result.taken ? "fail" : "pass"
field.status = cache[candidate]
on failure:
if candidate != field.value: return
field.status = "errored" # unknown, not invalidgo deeper
Know that a rule needing the server cannot answer instantly, so the field needs a waiting state and the check should not fire on every keystroke.
Explain debouncing, cancelling the superseded request, and matching a reply to the value it judged, plus why a failed request is not a failing value.
Show the production judgment: the submit policy for a pending check, per-value caching, gating the round trip behind cheap local rules, and the race that makes any client-side pass provisional.
Own the contract: which checks are worth a round trip at all, the debounce and rate-limit budget the backend is sized for, and the rule that shared-state invariants are enforced at write time rather than in the form.
Asynchronous validation - a check that needs something the client does not have, such as whether a username is already registered - breaks the assumption every synchronous rule relies on: that a verdict is about the value you are looking at. ## The five obligations 1. **Debounce the trigger.** Run the check after typing pauses, not on each edit. A trailing-edge delay of a few hundred milliseconds turns a dozen requests into one and matches the user's intent, since nobody wants a verdict about a half-typed name. Blur is also a sound trigger, and can be combined with the debounce so leaving the field flushes the pending check immediately. 2. **Cancel what is superseded.** When a new value arrives while a request is in flight, abort it with the platform's cancellation mechanism (an `AbortController` signal, or the transport's equivalent) or at minimum mark its result as ignorable. Without this, two replies can land in the wrong order and the older verdict wins. 3. **Model a pending state.** A field mid-check is in a third state. Pretending it is valid lets the user submit into a rejection; pretending it is invalid shows an error about a value that may be perfectly fine. Give it its own state, render a quiet indicator, and keep the submit control aware of it. 4. **Match the reply to the value.** Every reply carries, implicitly, the value it judged. Store that value with the request and compare on arrival: if the field has moved on, drop the result. This is the guard that makes out-of-order replies harmless even when cancellation is unavailable. 5. **Decide what submit does.** Three defensible policies: await the in-flight check before sending; send and let the server's verdict be authoritative; or block submit while anything is pending. What is not defensible is sending on the strength of a verdict about a different value. | State | Meaning | Submit behaviour | |---|---|---| | Idle | No check has run for this value | Run the check, or send and let the server judge | | Pending | A check is in flight for this value | Await it, or block, but never treat it as a pass | | Resolved pass | The server said free, for this exact value | Send; the value may still be taken by then | | Resolved fail | The server said taken, for this exact value | Block and show the message | | Errored | The check itself failed | Offer a retry; this is not a verdict about the value | ## The race nobody tests The check is inherently stale. Between a pass and the submit, another user can take the value. This is why an async client-side check is a *convenience* that shortens the feedback loop, and the server's own check at write time is the thing that actually holds the invariant. A form that treats the client's pass as the decision will eventually show a user a green field and then a rejection, so the submission path must be able to receive that rejection and present it - the mapping of a returned verdict onto a field belongs to the submission handling, but the timing conclusion belongs here: an async pass is a hint with a short shelf life. ## Cost control - **Cache per value.** Keep the resolved verdicts for values already checked, so returning to a previous value costs nothing and back-and-forth editing does not re-ask. - **Skip when a cheaper rule already fails.** Do not spend a round trip on a value the local format or length rule rejects; run the synchronous rules first and gate the async one behind them passing. - **Watch the amplification.** On a busy form, an undebounced check is a request per keystroke per field, which is a denial-of-service you wrote yourself. Rate-limiting on the server side is a reasonable expectation, not a substitute for the debounce. ## Failure is not a verdict If the request itself fails - offline, timeout, a server error - the honest state is *unknown*. Rendering the taken message teaches the user that a good value is bad. Rendering a pass lets them submit blind, which is acceptable only if the submit path handles the rejection well. The usual compromise: show an unobtrusive retryable notice, keep the field non-blocking, and let submit proceed to the authoritative check. ## Where reactivity models differ The obligations are identical everywhere; the ergonomics are not. A runtime that re-runs the component function on each state write needs the in-flight request and its cancellation held outside that re-run, or each keystroke creates a new one. A runtime with fine-grained tracking can express the check as a derived async value keyed by the field's value, with the framework discarding superseded results for you. A compile-time runtime can make the same derivation implicit. What no model does for you is decide the submit policy, the failure semantics, and the debounce interval - those are yours.
- Cancellation is already in place. Why also compare the reply against the field's current value?Because cancellation is best-effort. A request may already have been answered when the abort is issued, some transports deliver the reply anyway, and cached or retried responses can arrive late. Comparing the value the request was about with the value the field now holds is a cheap local guard that makes any late reply harmless, with or without a successful abort.
- The check says the value is free, but the submit is rejected as taken. Is the client's rule broken?No - it was right when it ran. Uniqueness is a property of shared state that can change between the check and the write, so a client-side pass is a hint with a short shelf life. The server must re-check at write time, and the form must be able to render the rejection it returns. Treating the earlier pass as a guarantee is the actual defect.
- What should the field show while the check is in flight?A pending indicator, and neither a pass nor a fail. Claiming valid invites a submit into a rejection; claiming invalid accuses a value that may be fine. The pending state should also be visible to the submit control, so pressing submit either waits for the outstanding check or proceeds knowingly to the authoritative one rather than acting on a guess.
- Should an async rule run on every change, or only on blur?Neither alone. A debounced change trigger gives feedback while the user is still in the field, and blur should flush any pending check so leaving the field resolves it. Running per change without a debounce is a request per keystroke; running only on blur means the user learns about a collision after they have moved on to the next field.
saying these in an interview costs you the question
- Fires the check on every keystroke with no debounce
- Treats a field mid-check as valid so submit proceeds
- Shows the taken message when the request itself failed
- Ignores out-of-order replies, so an older verdict wins
- Calls the client-side pass a guarantee that submit will succeed
- Spends a round trip on values the local format rule already rejects