What Is a Regular Expression?

A regular expression (regex or regexp) is a sequence of characters that defines a search pattern. It is used to find, match, extract, or replace text that fits a specific structure. Regex is available in virtually every programming language — JavaScript, Python, Java, Ruby, Go, Rust — and in command-line tools like grep and sed.

Regular Expressions for Engineers: Metacharacters, Groups, Lookaheads, and ReDoS

A regex can answer questions like:

  • Does this string contain a valid email address?
  • Extract all URLs from this HTML document.
  • Replace all ISO date strings with a different format.
  • Validate that this phone number follows a specific pattern.

The syntax looks cryptic at first, but it follows consistent rules. Once you internalize the building blocks, you can read and write regex confidently.


Metacharacters: The Building Blocks

Metacharacters are characters with special meaning inside a regex pattern.

Matching Characters

SymbolMeaningExampleMatches
.Any character except newlinea.c”abc”, “aXc”, “a1c”
\dAny digit (0–9)\d\d”42”, “00”, “99”
\DAny non-digit\D+”abc”, “foo”
\wWord char: [a-zA-Z0-9_]\w+”hello”, “user_1”
\WNon-word character\W” ”, ”!”, ”.”
\sWhitespace (space, tab, newline)a\sb”a b”, “a\tb”
\SNon-whitespace\S+”hello”, “123”

Anchors

SymbolMeaning
^Start of line/string
$End of line/string
\bWord boundary
\BNon-word boundary
/^hello/.test('hello world')  // true  — starts with "hello"
/^hello/.test('say hello')    // false — "hello" is not at the start
/world$/.test('hello world')  // true  — ends with "world"
/\bcat\b/.test('cat')         // true
/\bcat\b/.test('catch')       // false — "cat" is not at a word boundary

Quantifiers

SymbolMeaningGreedy?
*0 or moreYes
+1 or moreYes
?0 or 1Yes
{n}Exactly n times
{n,}n or more timesYes
{n,m}Between n and m timesYes
*? +? ??Non-greedy versionsNo

Greedy vs. Non-greedy:

const html = '<b>bold</b> and <i>italic</i>';

// Greedy — matches as much as possible
html.match(/<.+>/)[0]   // '<b>bold</b> and <i>italic</i>'

// Non-greedy — matches as little as possible
html.match(/<.+?>/)[0]  // '<b>'

Character Classes

A character class [...] matches any single character from the set.

[aeiou]       — any vowel
[a-z]         — any lowercase letter
[A-Z0-9]      — any uppercase letter or digit
[^aeiou]      — any character that is NOT a vowel (negation)
[a-z&&[^aei]] — intersection (Java syntax): lowercase consonants

The caret ^ inside a character class negates it. Outside [...], it anchors to the start of the string.


Groups and Captures

Capturing Groups (...)

Parentheses create a capturing group. The matched text is stored and can be retrieved later.

const date = '2026-01-14';
const match = date.match(/(\d{4})-(\d{2})-(\d{2})/);
console.log(match[1]); // '2026' — year
console.log(match[2]); // '01'   — month
console.log(match[3]); // '14'   — day

Named Capturing Groups (?<name>...)

Names make captures self-documenting:

const match = '2026-01-14'.match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/);
console.log(match.groups.year);  // '2026'
console.log(match.groups.month); // '01'

Non-Capturing Groups (?:...)

When you need grouping for alternation or quantifiers but do not need to capture the result:

// Match 'color' or 'colour', but don't need to capture 'ou?'
/colo(?:u?)r/  // matches both 'color' and 'colour'

Alternation |

The pipe | acts as OR between alternatives:

/cat|dog/.test('I have a cat') // true
/cat|dog/.test('I have a dog') // true
/(jpg|jpeg|png|gif)$/.test('photo.jpeg') // true

Lookaheads and Lookbehinds

Lookarounds let you match a pattern only if it is (or is not) preceded or followed by another pattern, without including the lookaround text in the match.

SyntaxNameMeaning
(?=...)Positive lookaheadFollowed by…
(?!...)Negative lookaheadNOT followed by…
(?<=...)Positive lookbehindPreceded by…
(?<!...)Negative lookbehindNOT preceded by…
// Positive lookahead: match "100" only if followed by " USD"
'100 USD'.match(/\d+(?= USD)/)[0]    // '100'
'100 EUR'.match(/\d+(?= USD)/)       // null

// Negative lookahead: match "file" not followed by ".bak"
'file.txt'.match(/file(?!\.bak)/)[0] // 'file'
'file.bak'.match(/file(?!\.bak)/)    // null

// Positive lookbehind: match digits preceded by "$"
'Price: $42'.match(/(?<=\$)\d+/)[0]  // '42'

Practical Patterns

Email Address (Simplified)

^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$

Note: A fully RFC 5321-compliant email regex is extremely complex. For most form validation purposes, a simplified pattern like the one above is sufficient. Always confirm email validity by sending a confirmation email, not just by regex matching.

URL (http/https)

^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&\/=]*)$

ISO 8601 Date (YYYY-MM-DD)

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

IPv4 Address

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

Hex Color Code

^#([0-9a-fA-F]{3}){1,2}$

Flags (Modifiers)

Flags change how the pattern is interpreted.

FlagMeaning
gGlobal — find all matches, not just the first
iCase-insensitive
mMultiline — ^ and $ match start/end of each line
sDotall — . also matches newlines
uUnicode — treat pattern and string as Unicode
'Hello WORLD'.match(/hello/i)[0] // 'Hello' — case-insensitive
'a\nb\nc'.match(/^\w/gm)         // ['a', 'b', 'c'] — multiline

ReDoS: Catastrophic Backtracking

ReDoS (Regular Expression Denial of Service) is a vulnerability that occurs when a maliciously crafted input causes a regex to run exponentially longer than expected.

The classic vulnerable pattern is nested quantifiers:

^(a+)+$     — matching "aaaaaaaaaaaaaaab" causes catastrophic backtracking

The regex engine tries all possible ways to group the as before concluding there is no match. For a string of length n, this can take O(2ⁿ) time.

Why This Matters

If your application accepts user input and passes it to a regex — or worse, lets users provide their own regex patterns — a malicious user can hang your server or consume excessive CPU with a single carefully crafted string.

How to Prevent ReDoS

  1. Avoid nested quantifiers: Never write (a+)+, (a*)*, (a+|b+)+, etc.
  2. Use atomic groups or possessive quantifiers where your regex engine supports them.
  3. Set timeouts when executing regexes against untrusted input.
  4. Test with long inputs: Try patterns against strings of length 20, 30, 50 with no match expected.
  5. Use static analysis tools: Libraries like safe-regex (npm) or vulture can detect dangerous patterns.
// Vulnerable
/^(a+)+$/.test('aaaaaaaaaaaaaaab'); // may hang

// Safe alternative — no nested quantifiers
/^a+$/.test('aaaaaaaaaaaaaaab');    // fast

Language Differences

While regex syntax is broadly similar across languages, there are notable differences:

FeatureJavaScriptPythonGoJava
Named groups(?<name>...)(?P<name>...)(?P<name>...)(?<name>...)
LookbehindSupported (ES2018+)SupportedNot supportedSupported
\d matches unicode digitsDepends on u flagDepends on re.UNICODEASCII onlyDepends on flag
Multiline defaultNoNoNoNo

FAQ

What is the difference between match and exec in JavaScript?

String.prototype.match() returns an array of results. With the g flag, it returns all matches. Without g, it behaves like RegExp.prototype.exec(), which also returns capture groups. exec() is useful in loops when using the g flag because it remembers the last match position via lastIndex.

How do I escape special characters in regex?

Special characters ., *, +, ?, (, ), [, ], {, }, ^, $, |, \ must be escaped with a backslash to be matched literally.

// Escape a period to match a literal dot, not "any character"
/\./.test('a.b') // true
/\./.test('axb') // false

A utility function to escape arbitrary strings:

function escapeRegex(str) {
  return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

Is regex the right tool for parsing HTML or JSON?

Generally no. HTML and JSON have recursive structures that regex cannot properly handle. Use a dedicated parser instead: DOMParser for HTML, JSON.parse() for JSON. Regex is appropriate for extracting simple patterns within already-parsed text fields.

What does the y (sticky) flag do in JavaScript?

The y flag makes the regex only match starting from the position indicated by lastIndex. Unlike g, it does not advance through the string looking for matches — it either matches at the current position or fails.