RX

Common Mistakes

Regular Expressions · 8 entries

Catastrophic Backtracking

syntax
Dangerous: (a+)+$  or  (.*a){10}  or  (x+x+)+y
example
JS:  // DANGER: This can freeze your browser/server!
     // /(a+)+$/.test('aaaaaaaaaaaaaaaaaaaaaaaaaab')
     // The engine tries exponential combinations before failing
Py:  # Same risk: re.search(r'(a+)+$', 'aaa...ab')
output
Extremely slow or hangs -- exponential backtracking

Note Catastrophic backtracking occurs when the regex engine explores an exponential number of paths. It happens with nested quantifiers applied to overlapping patterns. Prevention: avoid nested quantifiers on similar character sets, use atomic groups or possessive quantifiers, add anchors, or restructure the pattern. This is one of the most common causes of regex-based DoS (ReDoS) vulnerabilities.

Greedy vs Lazy Confusion

syntax
.* vs .*?
example
Input: '<b>one</b><b>two</b>'

JS:  '<b>one</b><b>two</b>'.match(/<b>.*<\/b>/)
     // Greedy: "<b>one</b><b>two</b>"  (too much!)
JS:  '<b>one</b><b>two</b>'.match(/<b>.*?<\/b>/)
     // Lazy: "<b>one</b>"  (correct!)
output
Greedy grabs everything, lazy stops at first delimiter

Note Developers often default to .* when .*? or a negated character class [^<]* would be correct. Rule of thumb: if you want to match UP TO a delimiter, either use lazy quantifiers or a negated class that excludes the delimiter character.

Forgetting to Escape the Dot

syntax
. matches ANY character, \. matches a literal period
example
// Wrong: matches 'a.b' but also 'axb', 'a5b', etc.
/a.b/.test('axb')     // true (unintended!)

// Correct: matches only 'a.b'
/a\.b/.test('axb')    // false
/a\.b/.test('a.b')    // true
output
Unescaped dot matches too broadly

Note This is the most frequent regex beginner mistake. Domains, IP addresses, file extensions, and version numbers all contain literal dots that must be escaped. When reviewing regex, always check that dots are escaped where literal periods are intended.

Forgetting to Escape Brackets

syntax
\[ \] for literal brackets, [...] creates a character class
example
// Wrong: trying to match '[INFO]'
/[INFO]/.test('N')    // true! It's a character class matching I, N, F, or O

// Correct:
/\[INFO\]/.test('[INFO] Server started')   // true
output
[INFO] without escaping matches any single character I, N, F, or O

Note Square brackets define character classes in regex. To match literal brackets, escape both opening and closing brackets. This mistake often appears when parsing log files with bracketed labels like [ERROR] or [WARN].

^ Inside vs Outside Character Class

syntax
^abc  (start of string)    [^abc]  (negation inside class)
example
// ^ outside brackets: start-of-string anchor
/^cat/.test('category')    // true (starts with 'cat')

// ^ inside brackets: negation
/[^cat]/.test('dog')       // true (matches 'd', 'o', 'g' -- none are c, a, or t)
output
Same symbol, completely different meanings based on context

Note The ^ character serves double duty. Outside a character class it anchors to the start of the string. As the first character inside [...], it negates the class. Anywhere else inside [...], it matches a literal caret. This dual meaning catches many beginners.

Multiline Flag Misconceptions

syntax
/m changes ^ and $ only -- NOT the dot
example
// Common mistake: thinking 'm' makes dot match newlines
'line1\nline2'.match(/line1.line2/m)   // null! Dot still skips \n

// You need the 's' flag for that:
'line1\nline2'.match(/line1.line2/s)   // matches
output
m flag: affects ^/$ only
s flag: affects dot only

Note The m (multiline) flag only changes ^ and $ to match line boundaries. It does NOT make the dot match newlines -- that is the s (dotall) flag. This is one of the most common sources of confusion, especially for developers coming from editors where 'multiline' implies dot-matches-all.

Over-Complex Patterns

syntax
Avoid: /^(?:(?:[a-zA-Z0-9._%+-]+)@(?:(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}))$/
Prefer: split into steps or use a validation library
example
// Instead of one monster regex for URL validation:
function isValidUrl(str) {
  try { new URL(str); return true; }
  catch { return false; }
}

# Python: use urllib.parse
from urllib.parse import urlparse
def is_valid_url(s):
    r = urlparse(s)
    return bool(r.scheme and r.netloc)
output
Simpler, more maintainable, and fewer edge case bugs

Note Regex is powerful but not always the right tool. When a pattern grows beyond 50-80 characters, consider breaking it into multiple smaller regex checks, using built-in parsers (URL, email, date), or using a purpose-built library. Readability and maintainability matter -- a future developer (including you) will need to understand and modify it.

Global Flag + test() Stateful Bug

syntax
/pattern/g.test()  remembers lastIndex between calls
example
JS:  const re = /a/g;
     re.test('abc')   // true (lastIndex becomes 1)
     re.test('abc')   // false! (searches from index 1, no 'a' found)
     re.test('abc')   // true (lastIndex reset to 0 after failure)
output
true, false, true -- alternating unexpectedly

Note Using a regex literal with the g flag and test() or exec() in a loop or repeated calls causes this stateful bug because lastIndex persists. Fix: omit the g flag for test(), or create a new regex each time, or manually reset re.lastIndex = 0 before each test.