Using setCustomValidity() on an HTML form control, how do you make the browser show your own error text — for example "passwords do not match" — and what breaks if you get one step wrong?
answer
- your string, not the browser's
- one field's rule needs another field
- it does not clear itself
- always set both branches
- empty string is the reset
basics
~20 sCall setCustomValidity("message") to mark a control invalid with your own text; the browser then blocks submission and shows that message. You must call setCustomValidity("") once the value becomes acceptable, or the control stays invalid forever.
solid answer
~50 s`setCustomValidity()` lets you inject a failure the attributes cannot express — a cross-field rule like "passwords do not match", or a server answer like "that username is taken". Passing a non-empty string sets the control's `customError` flag, makes it invalid, puts your text in `validationMessage`, and blocks submission; the browser shows your text where its own message would have gone. The trap is that the flag is **sticky**: it is not recomputed when the value changes. If you never call `setCustomValidity("")` the control is permanently invalid and the form can never be submitted, even after the user fixes it. So the discipline is to re-evaluate on every relevant `input` or `change` event and always set it to either your message or the empty string — never one branch without the other. The message only becomes visible when something reports, so on submit or on a `reportValidity()` call.
code
javascript · 13 linesconst password = document.querySelector('#password');
const confirm = document.querySelector('#confirm');
function syncConfirm() {
// Always set BOTH branches, or the error becomes permanent.
confirm.setCustomValidity(
confirm.value === password.value ? '' : 'Passwords do not match'
);
}
// Both fields must re-run it: the rule spans the pair.
password.addEventListener('input', syncConfirm);
confirm.addEventListener('input', syncConfirm);go deeper
Know that setCustomValidity takes a plain string, that a non-empty string makes the control invalid with your text, and that the empty string is how you clear it.
Explain that the custom error is sticky — never recomputed for you — so every handler run must set either the message or the empty string, and that clearing it does not override the other constraints.
Demonstrate the cross-field and async cases: listeners on both fields of a pair, an in-flight error so submission cannot race an unchecked value, and stale-response guards.
Own the boundary decision — use custom validity as the submission gate while your own markup carries the visible, styled, localized message, and make sure every client rule has a server-side counterpart that is the real authority.
## What the method is for The declarative constraints — `required`, `pattern`, `min`, `max`, `minlength` — only describe one field in isolation. Plenty of real rules are not like that: - "Password confirmation must equal the password" (depends on another field). - "End date must be after start date" (depends on another field). - "That username is already taken" (depends on a server). - "This coupon has expired" (depends on business state). `setCustomValidity(message)` is the escape hatch that lets any of those participate in the same machinery the built-in constraints use. ## What it changes Calling it with a non-empty string: - sets `element.validity.customError` to `true` and `element.validity.valid` to `false`; - makes `element.validationMessage` return your string; - makes the element match `:invalid`; - makes `checkValidity()` return `false` and blocks native form submission; - makes the browser display **your** text in the place its own message would have appeared. Calling it with the empty string clears the custom error. Note carefully what that does *not* do: it does not make the element valid. Clearing `customError` simply removes your addition; the element is still subject to `required`, `pattern` and the rest. ## The sticky-flag trap This is the entire reason the question gets asked. Built-in constraints are recomputed automatically whenever the value changes. A custom error is not — it stays exactly as you last set it until you set it again. The classic broken code: ```js confirm.addEventListener('input', () => { if (confirm.value !== password.value) { confirm.setCustomValidity('Passwords do not match'); } // no else — the error never goes away }); ``` The user mistypes once, fixes it, and the form is now unsubmittable with a message about a mismatch that no longer exists. The fix is to make every run of the handler set the state unconditionally: ```js function syncConfirm() { confirm.setCustomValidity( confirm.value === password.value ? '' : 'Passwords do not match' ); } confirm.addEventListener('input', syncConfirm); password.addEventListener('input', syncConfirm); ``` Note the second listener. The confirmation field's validity depends on a *different* field, so editing the password must re-evaluate the confirmation too — otherwise the user fixes the mismatch from the other side and the stale error survives. ## When the message is actually shown Setting a custom error does not display anything by itself. The string surfaces when something reports: an attempted submission, or an explicit `reportValidity()` call. Between those moments the control is silently invalid — it matches `:invalid`, and `checkValidity()` says `false`, but the user sees nothing. If you want feedback the moment the fields diverge, you render it yourself and use the custom validity purely as the gate that stops submission. ## Asynchronous rules Server-dependent messages fit the same shape, with one caveat: the answer arrives late. ```js async function checkUsername() { username.setCustomValidity('Checking…'); // invalid while we wait const taken = await isTaken(username.value); username.setCustomValidity(taken ? 'That username is taken' : ''); } ``` Marking the field invalid while the request is in flight prevents a submission from racing past an unchecked value, and the final call resolves it either way. Stale responses need the usual guard — ignore a reply whose value no longer matches the current one. And a server-side rule checked here still has to be enforced on the server: this is UI, not the decision. ## Localization and the alternative Because you supply the string, custom validity messages are in **your** language rather than the browser's — one of the few ways to get consistent wording out of the native bubbles. But the bubble itself is still the browser's: unstyled, transient, one at a time, and not in the DOM. Many design systems therefore use `setCustomValidity()` only as the invalidity *signal* — so the form cannot submit and `:invalid` matches — while rendering the visible text themselves in ordinary markup next to the field. ## Weak answers "Set the message once and the browser clears it when the user fixes the value" — it does not; you clear it. "`setCustomValidity('')` makes the field valid" — it only removes your error; `required` may still be failing. "You can pass HTML for a styled message" — the argument is plain text; nothing renders markup.
- Does setCustomValidity("") make the control valid?No. It clears only the `customError` flag. The control still has to satisfy its own attributes, so a field that is also `required` and empty stays invalid with `valueMissing` set. Clearing your error returns the control to whatever the built-in constraints say about it.
- For a confirm-password field, which elements do you attach the re-check to?Both. The confirmation's validity depends on the password field, so editing *either* one must re-run the comparison. Attaching only to the confirmation leaves a stale "do not match" error whenever the user resolves the mismatch by editing the password instead.
- How would you handle a rule that needs a server round trip, such as "username already taken"?Set a custom error while the request is in flight so a submission cannot race past an unchecked value, then set either the failure message or the empty string when it resolves, ignoring replies for values the user has since changed. The server must still reject a taken username on submit — this is only feedback.
- When does the user actually see the custom message?Only when something reports: a submit attempt, or an explicit `reportValidity()` call. Until then the control is silently invalid. If you want the message to appear as soon as the fields diverge, render it yourself and keep the custom validity purely as the gate that blocks submission.
saying these in an interview costs you the question
- The browser clears the custom error when the value changes
- setCustomValidity('') marks the field valid
- Setting a message immediately displays it
- Only the confirm field needs a listener
- You can pass HTML markup as the message