Regular expressions have a reputation for being write-only. In practice a small subset covers the overwhelming majority of real tasks, and three specific misunderstandings cause most of the pain.
Anchors: the most common omission
\d{4} matches four digits anywhere in the string. It happily matches inside abc1234xyz.
^\d{4}$ matches a string that is exactly four digits and nothing else.
If you are validating rather than searching, you almost always want anchors. A validation regex without them will accept input with arbitrary junk on either side, which is a genuine security issue when the value ends up in a query or a filename.
\b is the word-boundary anchor: \bcat\b matches cat in "the cat sat" but not in "concatenate".
Greedy versus lazy
This is the behaviour that surprises people most.
Input: <b>bold</b> and <i>italic</i>
Pattern: <.+>
Match: <b>bold</b> and <i>italic</i> ← the whole thing
+ is greedy: it takes as much as possible, then backtracks only enough to let the rest of the pattern succeed. Since the string ends with >, the greedy match swallows everything.
Adding ? makes it lazy, take as little as possible:
Pattern: <.+?>
Matches: <b>, </b>, <i>, </i>
A more precise approach avoids the question entirely: <[^>]+>, "angle bracket, then characters that are not a closing bracket". Negated character classes are usually clearer and faster than lazy quantifiers.
Groups
(...) captures, the matched text is available as $1, $2 and so on, which is what makes find-and-replace powerful:
Find: (\w+)@(\w+)\.com
Replace: $2 contact: $1
(?:...) groups without capturing, for when you only need the grouping for alternation or repetition.
(?<name>...) captures by name, which is worth the extra characters in any pattern you will read again.
Catastrophic backtracking
This is where regex stops being a convenience and becomes an outage.
Pattern: (a+)+b
Input: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
There is no b, so the match must fail, but the engine tries every way of dividing those as between the inner and outer quantifier before concluding that. The number of combinations grows exponentially, and a 30-character input can hang a thread.
This is a real denial-of-service vector, known as ReDoS, and it has taken down production systems. The pattern to watch for is nested quantifiers with overlapping alternatives: (a+)+, (a|a)*, (\s*\w+)*.
Avoid it by making sure each part of the pattern can only match in one way, and by testing patterns against long non-matching inputs rather than only against the input you expect.
Do not validate email addresses with regex
The RFC 5322 grammar for a valid email address permits quoted strings, comments, and IP-literal domains. A regex that fully implements it is several thousand characters long and still cannot tell you whether the address exists.
Use a sanity check, "has one @, has something before it, has a dot after it", and then send a confirmation email. That is the only test that answers the question you actually care about.
The same reasoning applies to URLs, phone numbers and postal addresses. Regex validation on these rejects legitimate real-world input constantly.
Patterns worth keeping
^\s+|\s+$ trim whitespace
\s{2,} runs of whitespace (replace with one space)
^\s*$\n blank lines
\d{4}-\d{2}-\d{2} ISO-style date
#?([0-9a-fA-F]{6}) hex colour
^(?=.*[a-z])(?=.*\d) lookahead: contains a lowercase letter and a digit
Lookahead (?=...) asserts something follows without consuming it, the standard tool for "must contain all of these" rules.
Test before you ship
Regex is dense and easy to get subtly wrong in ways that only appear on real data. Build patterns against actual sample text, including the cases you expect to fail. A regex tester with live highlighting turns a guessing game into a two-minute job.
For common extraction tasks, emails, URLs, phone numbers, a dedicated tool with a battle-tested pattern will do better than one you write in a hurry.