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.

6 min read

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:

htmlhtml
<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>
AttributeWhat it validates
requiredField must have a value
type="email"Must contain an @ and a domain
type="number"Must be a valid number
minlength / maxlengthMinimum and maximum character count
min / maxMinimum and maximum numeric value
patternMust 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:

MemberWhat it tells you or does
input.validity.validtrue if all constraints pass
input.validity.valueMissingtrue if required and empty
input.validity.typeMismatchtrue if type="email" and the value is invalid
input.validity.tooShorttrue if the value is below minlength
input.validationMessageThe 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:

javascriptjavascript
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():

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
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

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.
RunePowered by Rune AI

Frequently Asked Questions

Should I use HTML validation or JavaScript validation?

Use both. HTML attributes like required, type='email', and pattern give you free basic validation that works without JavaScript. Use JavaScript for complex rules (password strength, field comparisons, async checks) and to customize the appearance of error messages.

How do I show custom error messages instead of the browser's default popups?

Use the Constraint Validation API. Call setCustomValidity('message') on the input to set a custom error, then call reportValidity() to display it. For fully custom UI, use JavaScript validation directly and render your own error elements next to the fields.

Does the invalid event fire for every validation error?

The invalid event fires on individual form controls when they fail validation, typically during form submission. It fires once per invalid control per submission attempt. You can listen for it to show custom error UI when validation fails.

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.