← Back to Blog
·9 min read

Why Your Form Validation Regex Is Wrong (And What to Do Instead)

Form validation is where developers most commonly write regex, commit it to a codebase, and never revisit it — until a user files a bug report saying they cannot register with their perfectly valid email address. Here are the patterns developers get wrong and the philosophy that produces better outcomes.

The email validation debate is settled

The RFC 5322 compliant email regex is 6,400 characters. It handles every valid email format specified in the standard. It does not matter. Do not use it.

// The correct email validation regex for most applications:
/^[^s@]+@[^s@]+.[^s@]+$/

// What it checks:
// - Contains @ symbol
// - No whitespace anywhere
// - Something before @, something after @, a dot in the domain part

// It will accept: user+tag@subdomain.example.co.uk  ✓
// It will accept: "user name"@example.com            ✓ (valid RFC 5322)
// It will accept: very.unusual."@".unusual.com@example.com  ✓ (valid)
// It will not prevent: definitely@not@valid.com      ✗ (has two @)
// Wait, actually: /^[^s@]+@[^s@]+.[^s@]+$/ does prevent two @

// The important truth: validate format loosely, validate existence by sending email.
// A user who enters a typo in their email should find out when they try to log in,
// not when your regex incorrectly rejects their valid address.

The correct philosophy: accept anything that looks plausible as an email address and send a verification email. The only true email validation is whether the address receives mail. Regex can only check format — and the format of valid email addresses is wider than most developers realize.

Phone number validation: do not use regex

Phone number formats are globally diverse in ways that make regex validation nearly impossible to do correctly:

// Valid phone numbers from different countries:
+1 (555) 867-5309        // US — common format
+44 20 7946 0958         // UK — area code + number
+81 3-1234-5678          // Japan — Tokyo
+55 11 9 8765-4321       // Brazil — mobile with 9-digit format
+7 (495) 123-45-67       // Russia
+86 138 1234 5678        // China — mobile

// A regex that matches all of these without false positives
// while rejecting invalid numbers is approximately impossible.

// The correct solution: libphonenumber (Google's phone library)
// Available for: JavaScript, Python, Java, Go, Ruby, PHP, and more.
// It knows about every country's format, validates against actual number ranges,
// and formats numbers consistently.

import { parsePhoneNumber, isValidPhoneNumber } from 'libphonenumber-js';
isValidPhoneNumber('+1-555-867-5309', 'US')  // true

If you cannot use a library: accept any string of 7-15 digits (plus optional +, spaces, dashes, and parentheses) and validate existence by sending an SMS. The same philosophy as email.

URL validation: context determines the pattern

// For user-submitted URLs (blog post links, profile URLs):
// Use the URL constructor — it throws on invalid URLs:
function isValidUrl(str: string): boolean {
  try {
    const url = new URL(str);
    return url.protocol === 'http:' || url.protocol === 'https:';
  } catch {
    return false;
  }
}

// If you need regex (e.g., finding URLs in text):
/https?://[^s<>"']+/g
// Simple, handles most real-world URLs in text

// What NOT to do — the overly strict URL regex that rejects valid URLs:
// /^(https?://)?([da-z.-]+).([a-z.]{2,6})([/w .-]*)*/?$/
// This rejects: localhost, IPs, many valid TLDs, long paths

Password strength: validate requirements, not format

// WRONG: one giant regex that validates all requirements simultaneously
/^(?=.*[a-z])(?=.*[A-Z])(?=.*d)(?=.*[@$!%*?&])[A-Za-zd@$!%*?&]{8,}$/
// Hard to read, hard to give targeted error messages, wrong requirement set

// RIGHT: separate checks with specific feedback
function validatePassword(password: string): string[] {
  const errors: string[] = [];
  if (password.length < 8) errors.push("At least 8 characters");
  if (!/[a-z]/.test(password)) errors.push("At least one lowercase letter");
  if (!/[A-Z]/.test(password)) errors.push("At least one uppercase letter");
  if (!/d/.test(password)) errors.push("At least one number");
  return errors;
}
// Clear per-requirement feedback, easy to modify requirements,
// shows which specific conditions failed

The NIST guidelines (SP 800-63B) recommend against complexity requirements in favor of length. An 8-character minimum with a character diversity requirement produces weaker passwords than a 16-character minimum with no diversity requirement — because users respond to complexity rules by making predictable substitutions (P@ssw0rd). Whatever requirements you implement, the separate-checks pattern gives better user experience than a single opaque regex.

The right philosophy for form validation

The purpose of form validation is to help users provide usable data, not to enforce format purity. That distinction matters: validation should prevent clearly invalid input (an email with no @ symbol) while accepting anything that could plausibly be valid. The more restrictive your validation regex, the more valid inputs you reject — and those rejections cause users to give up on your product or provide fake data just to get past your form.

  • Validate format loosely. Check that the general shape is right, not that every edge case is covered.
  • Validate existence asynchronously. Email → send verification. Phone → send SMS. URL → try to load it.
  • Give specific error messages. "Invalid email" is useless. "Email must contain @" is actionable.
  • Never block form submission on formatting. Warn the user, let them confirm and submit anyway. They may know their edge-case email is valid.
  • Use libraries for complex formats. Phone numbers, IBAN numbers, postal codes — use an existing library rather than writing regex.

Published June 2, 2026 · By the utili.dev Team