Alternation
Alternation (the | operator) matches one option or another. Used everywhere — routing rules, file extensions, ICD codes — but it’s the regex feature most prone to wrong-precedence bugs and catastrophic backtracking when overused.
Grouping, anchors, order, performance
EXAMPLE
// 1) Basic alternation
/cat|dog/.test('I have a cat'); // true
/cat|dog/.test('I have a fish'); // false
// 2) Precedence — | has the LOWEST precedence
// /^cat|dog$/ means /(^cat)|(dog$)/
// matches 'cat...' at the start OR '...dog' at the end
// Often NOT what you wanted. Use a group:
/^(cat|dog)$/.test('cat'); // true
/^(cat|dog)$/.test('cats'); // false
// 3) Real-world: file extensions
const img = /\.(jpe?g|png|gif|webp|avif|svg)$/i;
img.test('photo.JPG'); // true
img.test('logo.svg'); // true
img.test('archive.tar.gz'); // false
// 4) Real-world: HTTP methods
const write = /^(POST|PUT|PATCH|DELETE)$/;
write.test('POST'); // true
write.test('GET'); // false
// 5) Order matters — first match wins, left to right
/Java|JavaScript/.exec('JavaScript');
// [ 'Java', index: 0 ] ← matched 'Java' before trying 'JavaScript'
// Put LONGER alternatives FIRST when one is a prefix of another
/JavaScript|Java/.exec('JavaScript');
// [ 'JavaScript', index: 0 ]
// 6) Anchors apply to the whole alternation when grouped
/^foo|bar$/.test('xbar'); // true — bar matches at end
/^(foo|bar)$/.test('xbar'); // false — anchored as a whole
// 7) Non-capturing groups — when you don't need the value
/^(?:cat|dog) food$/.test('cat food'); // true; (?:…) doesn't fill match[1]
// 8) Performance — alternation backtracks
// Each alternative is tried in order. With many alts and shared prefixes,
// the engine retries from the start every time.
// Bad: redundant prefixes
/cat|car|can|cap|cab/
// Better: factor the common prefix
/ca[trnpb]/
// or
/ca(?:t|r|n|p|b)/
// 9) Catastrophic backtracking — alternation inside repetition
// /^(a|a)*$/.test('a'.repeat(30) + '!')
// is exponential. ALWAYS make alternatives disjoint inside *, +, etc.
// Modern engines (RE2, Rust regex) avoid this. JS, Python re do not by default.
// 10) Character classes are not alternation
/[abc]/.test('a'); // matches a, b, or c — one char
/a|b|c/.test('a'); // same result, but slower
// Use [...] for single-char alternatives; reserve | for multi-char.
// 11) Word boundaries with alternation
/\b(cat|dog)\b/g // whole words only
'cathedral and catalog'.match(/\b(cat|dog)\b/g); // null — no whole word 'cat'
'a cat and dog'.match(/\b(cat|dog)\b/g); // ['cat', 'dog']
// 12) Branch-reset / DEFINE — advanced engines only
// PCRE supports (?| … | … ) for shared capture numbering. JS / Python lack this.
// 13) Real-world: parsing a date in multiple formats
const date = /^(\d{4})-(\d{2})-(\d{2})|^(\d{2})\/(\d{2})\/(\d{4})$/;
// Now you have to check which group set matched. Cleaner — split into two:
const iso = /^(\d{4})-(\d{2})-(\d{2})$/;
const us = /^(\d{2})\/(\d{2})\/(\d{4})$/;
function parse(s) {
let m;
if ((m = iso.exec(s))) return { y: +m[1], mo: +m[2], d: +m[3] };
if ((m = us.exec(s))) return { y: +m[3], mo: +m[1], d: +m[2] };
return null;
}
// 14) Common bugs
// • /^a|b$/ instead of /^(a|b)$/ — wrong anchor scope
// • Java|JavaScript order — short alternative wins
// • [a|b|c] — character class containing the literal | char
// • Long lists like /one|two|three|…/ — switch to allowlist Set lookup
Why it matters
Always wrap alternation in a group when anchors or quantifiers are involved — ^cat|dog$ almost never means what people expect. And put the longer alternative first when one is a prefix of another, or the shorter one matches and the longer never gets tried.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Discussion
Loading…