FIXR

The Regex Patterns Every Developer Copies From Stack Overflow (And Which Ones Are Wrong)

11 min readFIXR
RegexJavaScriptWeb Development

Nobody writes regex from scratch. Everyone copies it. The problem is that the most-copied patterns on the internet include several that are subtly, expensively wrong.

Here are the patterns worth keeping, the ones worth deleting, and the reasoning for each.


Email: stop trying to validate it properly

This is the canonical example of regex misuse. The pattern most frequently pasted into codebases:

/^[^\s@]+@[^\s@]+\.[^\s@]+$/

This is fine. Use it. What you should not use is the RFC 5322-compliant monster that circulates on Stack Overflow - it is roughly 6,000 characters long, and it still rejects valid addresses while accepting things no mail server will deliver to.

Why the simple version is correct in practice:

  • user+tag@gmail.com is valid and heavily used. Many "strict" patterns reject the +.
  • 名前@example.com is valid under internationalized email standards. ASCII-only patterns reject it.
  • admin@localhost has no TLD and is valid on internal networks.
  • Conversely, definitely-not-real@notarealdomain.invalid passes every regex ever written.

The only real email validation is sending a confirmation link. Regex should catch typos like a missing @, nothing more. Better still, use <input type="email"> and let the browser handle it.


URLs: use the URL constructor instead

Another case where regex is the wrong tool. JavaScript ships a parser:

function isValidUrl(str) {
  try {
    const url = new URL(str);
    return url.protocol === "http:" || url.protocol === "https:";
  } catch {
    return false;
  }
}

The protocol check matters. new URL("javascript:alert(1)") parses successfully. If you are validating a user-supplied link before rendering it in an href, omitting that check is an XSS vulnerability - this is a real and common bug.

If you must use regex, for extracting URLs from text rather than validating them:

/https?:\/\/[^\s<>"']+/g

Phone numbers: North American format

/^(\+?1[-.\s]?)?\(?([0-9]{3})\)?[-.\s]?([0-9]{3})[-.\s]?([0-9]{4})$/

Matches (555) 123-4567, 555-123-4567, +1 555 123 4567, 5551234567.

For international numbers, do not use regex. Use libphonenumber-js. Phone numbering plans are genuinely irregular and change; a library that is updated is the only maintainable answer.

A more forgiving approach that works well in practice - strip everything, then check length:

const digits = input.replace(/\D/g, "");
const valid = digits.length === 10 || (digits.length === 11 && digits[0] === "1");

Passwords: the lookahead pattern

/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&#])[A-Za-z\d@$!%*?&#]{8,}$/

Each (?=...) is a zero-width lookahead - it asserts a character class exists somewhere ahead without consuming anything.

Worth knowing: NIST no longer recommends composition rules like this. SP 800-63B explicitly advises against mandatory character-class requirements, because they push users toward predictable patterns (Password1!) while adding little entropy. Current guidance is: enforce a minimum length of 8 (ideally 12+), allow up to at least 64 characters, allow all Unicode including spaces, and check candidates against a breach corpus.

Length beats complexity. correct horse battery staple is stronger than P@ssw0rd and easier to remember. Our password strength checker scores by estimated entropy rather than by character classes, which is the more useful signal.


Dates

ISO 8601 (YYYY-MM-DD):

/^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/

This validates format, not existence. 2026-02-31 passes. Always follow up:

function isRealDate(s) {
  const [y, m, d] = s.split("-").map(Number);
  const dt = new Date(Date.UTC(y, m - 1, d));
  return dt.getUTCFullYear() === y
    && dt.getUTCMonth() === m - 1
    && dt.getUTCDate() === d;
}

Use Date.UTC rather than the local-time constructor - new Date("2026-03-08") versus new Date(2026, 2, 8) can land on different days depending on the user's timezone and DST.


Genuinely useful everyday patterns

Strip HTML tags (for display only, never for sanitization):

str.replace(/<[^>]*>/g, "")

To actually prevent XSS, use DOMPurify. Regex cannot parse HTML - this is famously true, and the Stack Overflow answer about it is correct.

Slugify:

str.toLowerCase()
   .normalize("NFD").replace(/[\u0300-\u036f]/g, "")  // strip accents
   .replace(/[^a-z0-9]+/g, "-")
   .replace(/^-+|-+$/g, "");

Collapse whitespace:

str.replace(/\s+/g, " ").trim()

Add thousands separators (though Intl.NumberFormat is better):

str.replace(/\B(?=(\d{3})+(?!\d))/g, ",")

Hex color:

/^#?([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i

IPv4 - note the ordering, which prevents 999:

/^((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/

Semantic version:

/^(\d+)\.(\d+)\.(\d+)(?:-([\w.-]+))?(?:\+([\w.-]+))?$/

Credit card format (Luhn check still required):

/^(?:4\d{12}(?:\d{3})?|5[1-5]\d{14}|3[47]\d{13}|6(?:011|5\d{2})\d{12})$/

The bug that will bite you: lastIndex

A regex literal with the g flag is stateful. .test() advances lastIndex, so repeated calls on the same object alternate between true and false:

const re = /\d+/g;
re.test("123");  // true
re.test("123");  // false  ← same input!
re.test("123");  // true

Fixes: drop the g flag when using .test(), create the regex inside the function, or reset re.lastIndex = 0 before each call. This bug is extremely common in validation helpers defined at module scope.


Catastrophic backtracking (ReDoS)

Nested quantifiers over overlapping character classes can go exponential:

/^(a+)+$/.test("aaaaaaaaaaaaaaaaaaaaaaaaaaaaX")  // hangs

The engine tries every possible way to partition the as before concluding there is no match. Add 10 more as and it takes a thousand times longer. If the input is user-controlled, this is a denial-of-service vector - and it has taken down production systems at Stack Overflow and Cloudflare.

Warning signs: (x+)+, (x*)*, (x|xy)+, (\s|\t)*$. Fixes: avoid nesting quantifiers, make inner classes mutually exclusive, cap repetition with {1,100}, or run untrusted patterns under a timeout.


Debug it, do not guess

Regex is written by iteration, not inspiration. Paste a pattern and real sample data into the regex tester to see live matches and capture groups, and browse the regex pattern library for vetted starting points.

Two flags worth using more:

  • u - proper Unicode handling, enables \p{...} property escapes like /\p{Emoji}/u
  • s - makes . match newlines, which is almost always what you meant for multiline input

Key takeaways

  • Simple email regex is correct; the RFC-compliant one is not worth it - confirmation emails are the real validation
  • Use new URL() for links and always check the protocol, or you have an XSS hole
  • Password composition rules are obsolete under current NIST guidance; length and breach-checking matter more
  • Date regex validates format, never existence - verify with a UTC Date round-trip
  • The g flag makes .test() stateful and is a frequent source of intermittent bugs
  • Nested quantifiers on user input create real denial-of-service risk

Tools mentioned in this article

Keep reading