Practical Regex Cheat Sheet: Patterns, Flags & Common Gotchas
Regular expressions are one of those tools every developer uses regularly and re-learns from scratch every few months, because the syntax is dense and the mental model doesn't stick between uses. This is a practical reference rather than a full tutorial: the patterns people actually search for, the flags that change how matching behaves, and the gotchas that cause the most head-scratching. Every example here works directly in our regex tester, which highlights matches inline and lists out each match's capture groups as you type.
Anchors: pinning a match to a position
^ matches the start of the string (or the start of a line, if the multiline flag is set), and $ matches the end of the string or line the same way. \b matches a word boundary — the transition between a word character (\w, i.e. a letter, digit, or underscore) and anything that isn't one — without consuming any characters itself, which makes it the right tool for matching a whole word like cat without also matching it inside category: the pattern \bcat\b does that, while plain cat does not.
A common mistake is forgetting that ^ and $ match the whole string by default, not each line — so a pattern meant to validate “every line starts with a number” needs the multiline flag (m) explicitly, or ^ will only ever match the very first line.
Character classes
A character class matches one character from a set. The built-in shorthand classes cover the cases people need most often: \d is any digit (0-9), \w is any word character (letters, digits, and underscore), \s is any whitespace character (space, tab, newline), and the uppercase versions — \D, \W, \S — match the exact opposite of each. A custom class in square brackets, like [aeiou], matches any single character listed inside it, and a leading ^ inside the brackets negates it — [^aeiou] matches any character that isn't a vowel. Ranges work inside brackets too: [a-z], [A-Z0-9], and so on.
One easy-to-miss detail: inside a character class, most special characters lose their special meaning. [.] matches a literal period, not “any character” — you don't need to escape the dot inside brackets the way you would outside them. The one character that keeps a special meaning inside brackets is ^ (negation, only when it's the first character) and, in some flavors, - if it isn't clearly part of a range.
Quantifiers, and greedy vs. lazy matching
* means zero or more, + means one or more, ? means zero or one, and {n,m} means between n and m repetitions (either bound can be omitted — {2,} means “2 or more”). By default all of these are greedy: they match as much as possible and only give characters back if that's required for the rest of the pattern to match at all.
This is the single most common source of “why did my regex match way more than I expected” bugs. Given the text <b>bold</b> and <i>italic</i> and the pattern <.+>, a greedy quantifier matches from the very first < all the way to the very last > — the whole string — rather than stopping at the first closing tag, because .+ greedily consumes everything it can and > is satisfied by the last one available. Adding ? after a quantifier makes it lazy instead — <.+?> matches as little as possible, stopping at the first > it can, which correctly isolates <b> alone. The rule of thumb: reach for a lazy quantifier whenever you're matching “everything up to the next occurrence of X,” and a greedy one whenever you genuinely want the longest possible match.
Email, URL, and phone matching — and their real caveats
A pattern like \w+@\w+\.\w+ — the default example loaded in our regex tester — matches the shape of a typical email address well enough for casual highlighting or a quick sanity check, but it's worth being honest about its limits. The actual grammar for valid email addresses (RFC 5322) is notoriously permissive — it technically allows quoted strings, comments, and characters like + and . in the local part — to the point that a fully spec-compliant regex is hundreds of characters long and still debated. In practice almost nobody needs full RFC compliance: a pragmatic pattern that accepts common real-world addresses and rejects obvious garbage, paired with an actual verification email, beats a monster regex chasing 100% correctness.
URL matching has a similar honesty problem: a simple pattern like https?:\/\/\S+ catches the common case but doesn't validate structure — a malformed host, invalid port, or bad encoding will still match. A robust URL validator has to account for internationalized domain names and query-string edge cases that make a simple regex the wrong tool; that job is better handed to a language's built-in URL parser (like JavaScript's URL constructor) once “does this look like a URL” needs to become “is this a valid URL.”
Phone number matching is the worst-behaved of the three: formats vary by country and even within one country (parentheses, dashes, spaces, country codes, extensions are all optional). A pattern loose enough to accept every legitimate format a user might type is usually too loose to reject much of anything, which is why most production systems normalize phone input as digits are typed rather than validating a final string against one pattern.
Flags: how matching behavior changes
Flags configure how a pattern is applied rather than what it matches. The six standard JavaScript flags are g (global — find all matches instead of stopping at the first one), i (ignore case), m (multiline — makes ^/$ match at line boundaries, not just string boundaries), s (dot-all — makes . also match newline characters, which it doesn't by default), u (unicode — treats the pattern and input as full Unicode code points rather than raw UTF-16 code units, which matters for emoji and other characters outside the Basic Multilingual Plane), and y (sticky — anchors matching to start exactly at lastIndex rather than searching forward).
The g flag specifically changes how repeated calls to .exec() behave: without it, calling .exec() on the same regex object always returns the first match; with it, each call picks up from where the previous one left off (tracked via the regex object's lastIndex property), which is how a single pattern object walks through every match in a string one at a time inside a loop.
Gotchas worth knowing by name
Catastrophic backtracking is the one that turns a working regex into a hung process. It happens when a pattern contains nested or ambiguous repetition — something like (a+)+b — applied against input that almost, but doesn't quite, match. The engine tries an exponential number of ways to split the repeated group before giving up, and what's instant on a 20-character string can take longer than the age of the universe on a 40-character one. The fix is almost always to make the repetition unambiguous: avoid nesting one unbounded quantifier directly inside another that could match the same characters.
Zero-length matches are the other subtle trap, and they show up directly in how a global match loop has to be written: a pattern like \b or x* can match successfully without advancing the string position, so a naive loop that just keeps calling .exec() will spin forever at the same index. The standard fix is to manually bump lastIndex forward by one whenever a match's start equals the regex's lastIndex afterward — exactly the guard a correct global-match implementation needs to stay safe on patterns capable of matching an empty string.
Forgetting to escape special characters in dynamic patterns — matching a literal period, dollar sign, or parenthesis that came from user input — is the last common gotcha: those characters mean something in regex syntax, so a literal . needs to be written as \., or the “any character” behavior of an unescaped dot will silently make the pattern too permissive.