Prompt and Applicable Context
Build the validation flow for a checkout form containing email, country, postal code, delivery date, and an optional discount code. Postal-code rules depend on country. The discount code is checked by a remote service. The server remains authoritative for prices, stock, address rules, and code validity.
The form must work with keyboard and assistive technology, at 200% zoom, and after a server round trip. Errors cannot be conveyed by color alone. A failed submission must preserve entered values, identify every actionable error, and provide a direct recovery path. Network failure, an expired code, and a newer async response overtaking an older one are part of the problem.
The goal is not to invent a form library. The answer should define state, validation timing, semantic relationships, focus policy, and the client-server error contract, then show how those decisions are verified. Native HTML constraints are the baseline; custom behavior must add clarity without removing browser semantics.
What the Interviewer Evaluates
The first signal is responsibility. Client validation provides fast feedback and avoids needless requests. Server validation decides whether an order is accepted. A candidate who trusts a client-side required attribute or discount total has left a security and correctness gap.
The second signal is recovery, not detection. An invalid border is insufficient. Each error needs plain text, a programmatic relationship to its control, and a correction instruction. On submission, users need an overview and a predictable place for focus. Their valid input must remain intact.
The third signal is timing. Showing “invalid” while a user is still typing an email creates noise. Validate on submit first; after a field has failed, revalidate it on blur or meaningful change so the user can see that the correction worked. Async validation needs a pending state and stale-response protection.
The final signal is verification. An automated accessibility scan can find missing labels, but cannot prove sensible announcements, focus order, recovery after a server response, or protection from an out-of-order discount-code response. Those require interaction tests.
Questions to Clarify Before Answering
- Which checks are local? Presence, format, and simple cross-field rules can run locally. Price,
stock, eligibility, and final discount validity are server-owned.
- When should feedback appear? Submit reveals all blocking errors. A field that has already failed
may revalidate on blur or after a deliberate change; do not announce on every keystroke.
- Will the server rerender the page? Both enhanced and ordinary form submission must preserve
values and render the same structured errors.
- Can one error affect several controls? Country and postal code form one rule. Put shared
instructions near the group and link the summary to the control that starts the correction.
- What happens during discount validation? Its pending state is informative, not an error. The
current value and request generation decide whether a response is still relevant.
- Where should focus move after failure? For several errors, focus a summary before the form; for a
compact single-error case, focusing the invalid control can be acceptable. Choose one policy and avoid announcing the same change twice.
- What failures are not field errors? A network outage or changed stock belongs in a form-level
message with a retry path. Do not attach it to email or postal code.
- Does the flow expose sensitive facts? Authentication and account-recovery forms may need generic
server messages. Helpful correction must not disclose whether an account exists.
30-Second Answer Framework
“I start with labeled native controls and constraints, but the server remains authoritative. Each invalid control gets aria-invalid="true" and a stable text error referenced by aria-describedby; color and icons are secondary. The first failed submit renders an error summary with links to every invalid field and moves focus to that summary. Entered values stay in place.
I validate on submit, then on blur or meaningful change only for fields that have failed. Discount-code checks are debounced, cancellable, and tagged with a generation so an old response cannot replace a new value. The server returns known field errors plus form-level errors in a structured shape. I test keyboard and screen-reader recovery, zoom and forced colors, server round trips, JavaScript-disabled submission, and async races in addition to automated checks.”
Step-by-Step Deep Dive
Step 1: Define state and invariants
Keep values separate from validation state. Each field can be untouched, pending, valid, or invalid, with an error code that maps to localized text. Form-level errors are separate. Store a submit attempt flag so the UI does not present a half-typed field as failed.
The invariants are: labels remain visible; instructions exist before an error; every displayed field error is programmatically associated; hidden errors are not referenced; one submission produces one clear focus move; valid input survives failure; and a stale async response cannot mutate current state.
Step 2: Build semantic controls before ARIA
Use label, the most specific input type, required, autocomplete, and appropriate length or pattern constraints. A country-dependent postal rule may use setCustomValidity(). Clear the custom message with an empty string as soon as the value becomes valid; otherwise the browser continues to block submission.
<label for="email">Email</label>
<input id="email" name="email" type="email"
aria-invalid="true"
aria-describedby="email-hint email-error">
<p id="email-hint">name@example.com</p>
<p id="email-error">Enter a valid email address.</p>checkValidity() checks constraints, while reportValidity() also asks the browser to present its feedback. If the product renders its own accessible messages, intercept the invalid state consistently instead of showing two competing error systems.
Step 3: Choose validation timing deliberately
On the first submit, validate every field and reveal all blocking errors. Afterward, revalidate an invalid field on blur. For selections or a corrected value with an unambiguous complete shape, a change can clear the error sooner. Do not repeatedly send live-region announcements while a date or email is incomplete.
Cross-field rules run when either dependency changes. If country changes, revalidate postal code and update its visible instruction. Do not silently rewrite user input unless the normalization is safe and reversible; trimming surrounding whitespace is different from guessing a postal format.
Step 4: Present inline errors and a navigable summary
Render concise text beside each field, set aria-invalid only while invalid, and reference the error's stable ID. Keep visual treatment usable in forced-colors mode and include words, not only red borders or warning icons.
After a failed multi-error submit, insert a titled summary before the form. Give it a temporary focus target, focus it once, state the error count, and provide links to the related controls. The link text should name the field and correction. Do not also move focus to the first field in the same event; that would make the summary hard to inspect.
Step 5: Make asynchronous validation race-safe
Wait until the discount code is complete, then debounce the request. Abort the previous request when the value changes and also compare a monotonically increasing generation on completion. Both checks matter because cancellation can arrive after a response has already progressed.
Show a nearby “Checking” status through a polite live region. Disable applying the discount while the check is pending, but do not disable unrelated fields. A timeout becomes a retryable form-level or code-level status according to the product contract; it must not be reported as “invalid code.” Cache results only with the pricing context and expiry that make them valid.
Step 6: Reconcile authoritative server errors
Submit raw values over the form's ordinary action or an enhanced request. The server normalizes and validates again, calculates the total, and returns a result such as field error codes, form error codes, and the accepted values. The client maps only allowlisted field names. Unknown keys become a safe form-level error rather than a selector or markup injection opportunity.
On rejection, preserve all non-sensitive entries, replace client guesses with server truth, render the same summary, and focus it. On success, show an unambiguous confirmation and prevent duplicate activation. If the response is lost after submission, use an idempotency key or order lookup before encouraging another charge.
Step 7: Verify recovery paths, not snapshots
Test empty submission, one error, several errors, country/postal dependency, expired delivery date, discount timeout, invalid discount, rapid code changes, server-only rejection, and lost success response. Assert values remain, summary links focus the right controls, corrected errors disappear, and old async responses are ignored.
Manually complete the flow using keyboard only and a screen reader. Check 200% zoom, reflow, visible focus, forced colors, browser autofill, and ordinary server submission without client JavaScript. Automated accessibility and unit tests supplement these checks; they do not replace announcement and focus testing.
Strong Sample Answer
“I would model field values independently from touched, pending, and error state. Native labels, input types, autocomplete tokens, and constraints supply the baseline. A custom cross-field rule uses setCustomValidity() and always clears the message when valid. Client checks improve speed; the server revalidates prices, stock, addresses, and discounts before accepting the order.
The first submit reveals every blocking error. Each invalid input receives aria-invalid="true" and references visible corrective text with aria-describedby. For multiple errors I render a summary before the form, focus it once, and link each item to its control. Valid values remain untouched. After that submit, failed fields revalidate on blur or meaningful change, without a live announcement on every keypress.
Discount validation is debounced and carries both an abort signal and a generation number. Only the response matching the current value can update the UI. Pending and unavailable are distinct from invalid. Server field codes map through an allowlist; global failures stay at form level. I would ship only after keyboard, screen-reader, zoom, no-script server round-trip, race, and duplicate-submit tests pass alongside automated checks.”
Common Mistakes
- Using a red border alone → some users cannot perceive the state → **add corrective text,
programmatic association, and a non-color cue.**
- Validating every keystroke → incomplete input creates constant noise → **validate on submit,
then revalidate failed fields at a useful boundary.**
- Leaving
setCustomValidity()populated → corrected input remains invalid → **set it to an empty
string when the rule passes.**
- Focusing the summary and first field → announcements and focus compete → **make one deliberate
focus move and provide links for navigation.**
- Treating a timeout as an invalid code → infrastructure failure becomes false user blame →
represent pending, unavailable, and invalid separately.
- Accepting the last response received → stale validation overwrites current input → **abort old
work and compare request generations.**
- Trusting client totals → requests can bypass the UI → recalculate and validate on the server.
- Clearing the form after failure → recovery becomes re-entry → **preserve valid, non-sensitive
values and replace only error state.**
Follow-up Questions and Answers
Follow-up 1: Should the summary use role="alert"?
For dynamically inserted errors, an alert or suitable live region can announce the summary. Focus can also make it available. Test the chosen combination because focus plus an assertive alert may announce the same content twice. On a full server navigation, putting the error count in the page title and main heading can provide earlier context.
Follow-up 2: Why not disable the submit button until the form is valid?
A disabled button can hide what remains wrong and is unreachable to some navigation modes. Keep a submit path available so the user can request a complete validation result. Disable only while an actual submission is in flight, explain that state, and recover if it fails.
Follow-up 3: When is aria-describedby preferable to a live region?
aria-describedby gives the field its current hint and error when the user reaches it. A live region announces a meaningful dynamic change without focus. Static inline errors do not all need live regions; announcing every field at once is noisy. Use a summary for the batch and polite status for truly asynchronous changes.
Follow-up 4: How do you support server-rendered validation without JavaScript?
Use a real form action. The server returns the same page with preserved values, field error codes, a summary before the form, and an error count in the title or heading. Summary links use stable control IDs. Client enhancement should consume the same error contract, so the two modes do not drift.
Follow-up 5: How would you test the async race deterministically?
Control two validation promises. Start request A, change the value, then start B. Resolve B as valid and A later as invalid. Assert that the UI still reflects B and the current value. Repeat with abort, timeout, unmount, and resubmission while pending.