JavaScript by Example

Form Validation

Checking user input before submission - HTML-native constraints, JavaScript custom messages, and the server-must-re-validate rule that client validation never replaces.

Form validation checks that user input meets your requirements before it is submitted. The browser handles common cases through HTML attributes alone; JavaScript steps in for cross-field rules, custom error messages, and live feedback. This lesson runs in a browser; the earlier lessons run anywhere JavaScript runs.

HTML provides built-in constraints through attributes like required, type, minlength, and pattern. The browser enforces them automatically when the form submits, shows native error bubbles, and exposes them via the :invalid CSS pseudo-class for styling.

<form id="signup">
  <input
    type="email"
    name="email"
    required
    placeholder="you@example.com"
  />
  <input
    type="password"
    name="password"
    required
    minlength="8"
  />
  <button type="submit">Sign up</button>
</form>
const form = document.querySelector("#signup");
 
// Check validity programmatically without submitting
console.log(form.checkValidity()); // false if any constraint fails
 
// Access individual field validity state
const emailField = form.querySelector("input[type='email']");
console.log(emailField.validity.valueMissing);   // true if empty
console.log(emailField.validity.typeMismatch);   // true if not a valid email
/* Style invalid fields after the user interacts (not on page load) */
input:user-invalid {
  border-color: red;
}

setCustomValidity lets you inject your own error message into the browser's built-in validity system. Call it with a non-empty string to mark a field invalid, then call reportValidity() to show the error to the user. Clear it with an empty string once the field is valid again.

const form = document.querySelector("#signup");
const password = form.querySelector("input[name='password']");
const confirm = form.querySelector("input[name='confirm']");
 
form.addEventListener("submit", (event) => {
  // Cross-field rule: passwords must match
  if (password.value !== confirm.value) {
    confirm.setCustomValidity("Passwords do not match.");
    confirm.reportValidity(); // shows the native error bubble
    event.preventDefault();   // stop the form from submitting
    return;
  }
 
  // Clear any previous custom error before submitting
  confirm.setCustomValidity("");
});

Live validation gives instant feedback as the user types. Running it on every input event can feel aggressive, especially for fields like email where the address isn't complete mid-type. A short debounce (waiting for a pause in typing) avoids false errors.

const emailField = document.querySelector("input[type='email']");
const errorEl = document.querySelector("#email-error");
 
let debounceTimer;
 
emailField.addEventListener("input", () => {
  clearTimeout(debounceTimer);
 
  debounceTimer = setTimeout(() => {
    if (emailField.validity.typeMismatch || emailField.validity.valueMissing) {
      errorEl.textContent = "Enter a valid email address.";
      emailField.setAttribute("aria-describedby", "email-error");
    } else {
      errorEl.textContent = "";
      emailField.removeAttribute("aria-describedby");
    }
  }, 400); // wait 400 ms after the user stops typing
});

In production

Client-side validation is a UX feature, not a security control. Any user can open the browser console, remove the required attribute, and submit whatever they want. The server must re-validate every input before acting on it. When displaying errors, place the message next to the failing field and connect it with aria-describedby so screen readers announce the error context. Never use alert() for validation feedback - it blocks the UI and is inaccessible.

Enjoyed this? Get more essays on software craft delivered to your inbox.

Subscribe free