Form Validation with JavaScript: Complete Guide
Learn how to validate forms with JavaScript using the Constraint Validation API, custom error messages, real-time validation patterns, and HTML attributes combined with JavaScript checks.
Form validation ensures the data a user submits is correct, complete, and in the right format before it reaches your server. JavaScript validation happens in the browser, gives instant feedback, and prevents bad submissions without a page reload.
There are two layers to client-side validation: HTML constraint attributes and JavaScript. They work together. HTML handles simple rules without any code. JavaScript handles everything else.
HTML Validation: Your First Line of Defense
HTML5 provides built-in validation attributes that work without JavaScript:
<form>
<input type="email" required placeholder="Email" />
<input type="password" required minlength="8" placeholder="Password" />
<input type="text" pattern="[A-Za-z]+" placeholder="Letters only" />
<button type="submit">Submit</button>
</form>| Attribute | What it validates |
|---|---|
required | Field must have a value |
type="email" | Must contain an @ and a domain |
type="number" | Must be a valid number |
minlength / maxlength | Minimum and maximum character count |
min / max | Minimum and maximum numeric value |
pattern | Must match the given regex |
If the user tries to submit a form that does not pass HTML validation, the browser blocks the submission and shows a built-in tooltip on the first invalid field. No JavaScript required.
The Constraint Validation API
The Constraint Validation API gives you programmatic control over HTML5 validation. Every form control exposes a validity object with these properties, plus a few methods for checking and reporting errors:
| Member | What it tells you or does |
|---|---|
input.validity.valid | true if all constraints pass |
input.validity.valueMissing | true if required and empty |
input.validity.typeMismatch | true if type="email" and the value is invalid |
input.validity.tooShort | true if the value is below minlength |
input.validationMessage | The browser's built-in error message |
input.checkValidity() | Returns true/false, fires the invalid event if false |
input.reportValidity() | Same as checkValidity(), but also shows the browser's error tooltip |
input.setCustomValidity(msg) | Sets a custom error message, or clears it when passed "" |
Here is a practical example using several of these together:
const emailInput = document.querySelector('input[type="email"]');
emailInput.addEventListener("change", () => {
if (emailInput.validity.typeMismatch) {
emailInput.setCustomValidity("Please enter a valid email address.");
} else if (emailInput.validity.valueMissing) {
emailInput.setCustomValidity("Email is required.");
} else {
emailInput.setCustomValidity(""); // Clear custom error
}
});The key detail: setCustomValidity("") with an empty string clears the custom error. If you forget to clear it, the input stays invalid even after the user corrects the value.
Validating on Submit: The Full-Form Check
The most reliable validation runs when the user submits the form. Use the submit event combined with checkValidity():
form.addEventListener("submit", (event) => {
if (!form.checkValidity()) {
event.preventDefault();
form.querySelector(":invalid").focus();
return;
}
console.log("All valid. Proceeding with submission.");
});form.checkValidity() returns true only if every field in the form passes validation. It also triggers the invalid event on each failing field, which you can use for custom error display.
Custom Error Messages with Inline UI
Browser default tooltips are functional but not customizable. For a polished experience, render your own error messages next to each field. A small helper checks one field's validity state and writes a matching message into its error element:
function showFieldError({ input, errorEl }) {
errorEl.textContent = "";
if (input.validity.valueMissing) {
errorEl.textContent = "This field is required.";
} else if (input.validity.typeMismatch) {
errorEl.textContent = `Please enter a valid ${input.type}.`;
} else if (input.validity.tooShort) {
errorEl.textContent = `Must be at least ${input.minLength} characters.`;
}
return errorEl.textContent !== "";
}The submit handler pairs each input with its error element, runs showFieldError on all of them, and blocks the submission if any field failed:
const fields = [
{ input: document.querySelector("#username"), errorEl: document.querySelector("#username-error") },
{ input: document.querySelector("#email"), errorEl: document.querySelector("#email-error") },
];
form.addEventListener("submit", (event) => {
const hasErrors = fields.map(showFieldError).some(Boolean);
if (hasErrors) {
event.preventDefault();
}
});This gives you full control over the wording, styling, and placement of error messages, since each field's error element is rendered by your own code instead of the browser's default tooltip.
Real-Time Validation: Catching Errors Early
Waiting until submit to show errors works but feels slow. Validate individual fields on change (when the user finishes editing) for a smoother experience:
const emailInput = document.querySelector("#email");
const emailError = document.querySelector("#email-error");
emailInput.addEventListener("change", () => {
if (emailInput.validity.valid) emailError.textContent = "";
else if (emailInput.validity.typeMismatch) emailError.textContent = "Please enter a valid email.";
else if (emailInput.validity.valueMissing) emailError.textContent = "Email is required.";
emailInput.classList.toggle("invalid", !emailInput.validity.valid);
});Validate on change, not input. If you validate on every keystroke, the user sees "Invalid email" after typing the first character. Wait until they finish editing (blur) before showing errors.
Common Validation Patterns
Password strength meter. Use input here so the meter updates on every keystroke instead of waiting for the field to lose focus:
passwordInput.addEventListener("input", () => {
const value = passwordInput.value;
let strength = 0;
if (value.length >= 8) strength++;
if (/[A-Z]/.test(value)) strength++;
if (/[0-9]/.test(value)) strength++;
if (/[^A-Za-z0-9]/.test(value)) strength++;
const labels = ["Weak", "Fair", "Good", "Strong"];
strengthMeter.textContent = labels[strength] || "Weak";
});Confirm password match. This check depends on the value of another field, so it belongs on change rather than a constraint attribute:
const confirmInput = document.querySelector("#confirm-password");
confirmInput.addEventListener("change", () => {
if (confirmInput.value !== passwordInput.value) {
confirmInput.setCustomValidity("Passwords do not match.");
} else {
confirmInput.setCustomValidity("");
}
});Checkbox required. Require at least one checkbox in a group to be checked, which no single HTML attribute can express on its own:
const checkboxes = document.querySelectorAll('input[name="interests"]');
form.addEventListener("submit", (event) => {
const anyChecked = Array.from(checkboxes).some((cb) => cb.checked);
if (!anyChecked) {
event.preventDefault();
checkboxError.textContent = "Select at least one interest.";
}
});HTML required on a checkbox only requires that specific checkbox, not the group as a whole, so it cannot express "select at least one." JavaScript gives you the finer control HTML cannot.
Client-Side vs Server-Side Validation
Client-side validation is for user experience. It is fast, gives instant feedback, and reduces server load. But it is not a security measure. Users can bypass it by disabling JavaScript, using browser devtools to remove required attributes, or sending requests directly with tools like curl.
Always validate on the server as well. Client-side validation catches honest mistakes. Server-side validation catches everything else. Never trust data from the client.
Common Mistakes
Not clearing setCustomValidity. After calling setCustomValidity("error"), the field stays invalid until you call setCustomValidity(""). Always clear the custom error in the else branch or on input.
Validating on input for complex rules. If you run heavy validation (regex, async checks) on every input event, the UI lags on fast typists. Debounce expensive validation or move it to change.
Showing all errors at once on an empty form. When the form first loads, all fields are empty. Do not show validation errors until the user has interacted with the form. Track whether the form has been submitted once or track per-field interaction.
Relying only on HTML validation attributes. HTML validation is a great first pass but cannot handle cross-field checks (password match), custom logic (username availability), or custom error UI. Use JavaScript for these cases, layering it on top of HTML attributes. For more on the form submission flow, see /javascript/javascript-form-handling-and-submission-tutorial. For knowing which event to use when validating (input vs change vs submit), see /javascript/javascript-input-change-and-submit-events-guide.
Rune AI
Key Insights
- Use HTML validation attributes (required, type, pattern, minlength) as your first layer.
- The Constraint Validation API gives you checkValidity(), reportValidity(), and setCustomValidity().
- Validate on change for individual fields and on submit for the whole form.
- setCustomValidity('') clears a custom error; set it to a non-empty string to set one.
- Show errors next to the relevant field, not in a generic alert.
- Always validate on the server too; client-side validation is for UX, not security.
Frequently Asked Questions
Should I use HTML validation or JavaScript validation?
How do I show custom error messages instead of the browser's default popups?
Does the invalid event fire for every validation error?
Conclusion
Form validation is a combination of HTML attributes for basic rules and JavaScript for custom logic. Use the Constraint Validation API to check validity, set custom messages, and control when errors appear. The best validation catches mistakes early, shows clear messages next to the problem field, and does not block the user unnecessarily.
More in this topic
Using Decorators for Logging in JS Architecture
Learn how JavaScript decorators wrap class methods to add logging without touching the original code, including setup with Babel and how execution order works.
JavaScript While Loop Explained: A Complete Guide
A while loop repeats code as long as a condition stays true. Learn the syntax, see practical examples, and understand when a while loop is the right choice over a for loop.
JavaScript Loops Tutorial: for, while, do while
Loops let JavaScript repeat code without writing it twice. Learn the three core loop types, what each one does, and when to choose one over another.