How to Use Alternation in JavaScript Regular Expressions
When a regular expression needs to match one pattern or another, alternation is the tool you reach for. The pipe character | acts as an OR operator inside regex, letting you define multiple alternative patterns that can all produce a valid match. It is one of the most frequently used regex features, yet its behavior around scope and grouping catches many developers off guard.
In this guide, you will learn exactly how the | operator works in JavaScript, how it interacts with the rest of your pattern, why grouping matters so much when using it, and how to avoid the most common mistakes.
The | Operator: OR in Regular Expressions
The | character separates two or more alternatives. The regex engine tries each alternative from left to right and returns the first one that matches.
Basic Syntax
let pattern = /cat|dog/;
console.log(pattern.test("I have a cat")); // true
console.log(pattern.test("I have a dog")); // true
console.log(pattern.test("I have a bird")); // false
Output:
true
true
false
The pattern /cat|dog/ means: match the text "cat" or the text "dog". The engine first tries "cat" at the current position. If that fails, it tries "dog". If both fail, it moves forward one character and tries again.
Multiple Alternatives
You can chain as many alternatives as you need:
let color = /red|green|blue|yellow|orange/;
console.log(color.test("The sky is blue")); // true
console.log(color.test("Roses are red")); // true
console.log(color.test("The wall is purple")); // false
Output:
true
true
false
Extracting the Match
Using match(), you can see which alternative actually matched:
let animal = /cat|dog|fish|bird/;
console.log("My dog is friendly".match(animal));
Output:
[ 'dog', index: 3, input: 'My dog is friendly', groups: undefined ]
With the g flag, you can find all occurrences:
let fruit = /apple|banana|cherry/g;
let text = "I like apple pie, banana bread, and cherry tart with apple sauce.";
console.log(text.match(fruit));
Output:
[ 'apple', 'banana', 'cherry', 'apple' ]
How Alternation Scope Works
This is where most confusion arises. The | operator splits the entire pattern into alternatives, not just the characters immediately next to it.
The Scope Problem
Consider this example:
// Intention: match "cats" or "dogs"
let pattern = /cats|dogs/;
console.log(pattern.test("cats")); // true
console.log(pattern.test("dogs")); // true
This works perfectly. But what if you want to match "gray cat" or "gray dog"?
// WRONG: this does NOT do what you think
let bad = /gray cat|dog/;
console.log(bad.test("gray cat")); // true
console.log(bad.test("gray dog")); // false!
console.log(bad.test("dog")); // true!
Output:
true
false
true
The pattern /gray cat|dog/ does not mean "gray cat or gray dog." It means "gray cat" or "dog". The | splits the entire expression into two completely separate alternatives:
| Alternative 1 | Alternative 2 |
|---|---|
gray cat | dog |
Everything to the left of | is one alternative. Everything to the right is the other.
Visualizing the Split
// What you wrote:
/gray cat|dog/
// What the engine sees:
// Alternative 1: "gray cat"
// Alternative 2: "dog"
// What you probably wanted:
// "gray " followed by ("cat" or "dog")
This is the single most important thing to understand about alternation: | has the lowest precedence of any regex operator. It separates the entire pattern unless you use grouping to limit its scope.
Alternation Inside Groups
To restrict the scope of |, wrap the alternatives in parentheses. This is where alternation becomes truly useful and predictable.
Using Capturing Groups
// CORRECT: alternation is scoped to the group
let pattern = /gray (cat|dog)/;
console.log(pattern.test("gray cat")); // true
console.log(pattern.test("gray dog")); // true
console.log(pattern.test("dog")); // false ("gray " is required)
Output:
true
true
false
Now the | only applies within the parentheses. The full pattern means: match "gray " followed by either "cat" or "dog".
Extracting the match shows the captured alternative:
let pattern = /gray (cat|dog)/;
let match1 = "I have a gray cat".match(pattern);
console.log(match1[0]); // "gray cat" (full match)
console.log(match1[1]); // "cat" (captured group)
let match2 = "I have a gray dog".match(pattern);
console.log(match2[0]); // "gray dog"
console.log(match2[1]); // "dog"
Output:
gray cat
cat
gray dog
dog
Using Non-Capturing Groups
If you do not need to capture which alternative matched, use a non-capturing group (?:...) to avoid creating an unnecessary capture:
let pattern = /gray (?:cat|dog)/;
let match = "I have a gray cat".match(pattern);
console.log(match[0]); // "gray cat"
console.log(match[1]); // undefined (no capturing group)
Output:
gray cat
undefined
Use non-capturing groups (?:...) when you only need grouping for alternation scope and do not care about extracting the matched alternative. This keeps your capture groups clean and numbered correctly.
Comparing Capturing vs Non-Capturing
// With capturing group (group 1 captures the animal)
let capturing = /I have a (cat|dog|fish)/;
let m1 = "I have a dog".match(capturing);
console.log(m1[1]); // "dog"
// With non-capturing group (no capture)
let nonCapturing = /I have a (?:cat|dog|fish)/;
let m2 = "I have a dog".match(nonCapturing);
console.log(m2[1]); // undefined
Output:
dog
undefined
The non-capturing version is preferable when you have other capturing groups in the pattern and do not want the alternation to shift their numbering:
// Problem: alternation group takes group 1, pushing the date to group 2
let bad = /(Mr|Mrs|Ms)\.\s+(\w+)/;
let m = "Mrs. Smith".match(bad);
console.log(m[1]); // "Mrs" (you might not need this captured)
console.log(m[2]); // "Smith"
// Better: non-capturing group for title, name stays as group 1
let good = /(?:Mr|Mrs|Ms)\.\s+(\w+)/;
let m2 = "Mrs. Smith".match(good);
console.log(m2[1]); // "Smith" (clean group numbering)
Output:
Mrs
Smith
Smith
Practical Examples
Matching File Extensions
let imageFile = /\w+\.(?:png|jpg|jpeg|gif|webp|svg)$/i;
console.log(imageFile.test("photo.jpg")); // true
console.log(imageFile.test("logo.PNG")); // true
console.log(imageFile.test("icon.svg")); // true
console.log(imageFile.test("data.csv")); // false
console.log(imageFile.test("script.js")); // false
Output:
true
true
true
false
false
Matching Date Formats
// Match dates with different separators: 2024-01-15, 2024/01/15, 2024.01.15
let datePattern = /\d{4}[-/.]\d{2}[-/.]\d{2}/;
console.log(datePattern.test("2024-01-15")); // true
console.log(datePattern.test("2024/01/15")); // true
console.log(datePattern.test("2024.01.15")); // true
Output:
true
true
true
Note that in this case, the separators are single characters, so a character class [-/.] is simpler and more efficient than alternation (?:-|\/|\.). Character classes are preferred for single-character alternatives.
But what if the separators are multi-character or you need the separator to be consistent?
// Ensure the same separator is used throughout (backreference)
let consistentDate = /\d{4}([-/.])\d{2}\1\d{2}/;
console.log(consistentDate.test("2024-01-15")); // true
console.log(consistentDate.test("2024/01/15")); // true
console.log(consistentDate.test("2024-01/15")); // false (mixed separators)
Output:
true
true
false
Matching HTTP Methods
let httpMethod = /^(?:GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)$/;
console.log(httpMethod.test("GET")); // true
console.log(httpMethod.test("POST")); // true
console.log(httpMethod.test("DELETE")); // true
console.log(httpMethod.test("FETCH")); // false
console.log(httpMethod.test("get")); // false (case-sensitive)
Output:
true
true
true
false
false
Matching Protocol Prefixes
let url = /^(?:https?|ftp|ssh):\/\//;
console.log(url.test("http://example.com")); // true
console.log(url.test("https://example.com")); // true
console.log(url.test("ftp://files.example.com")); // true
console.log(url.test("ssh://server.com")); // true
console.log(url.test("smtp://mail.com")); // false
Output:
true
true
true
true
false
Notice how https? inside the alternation uses the ? quantifier to make the s optional, matching both "http" and "https" as a single alternative.
Matching Keywords in Code
let jsKeyword = /\b(?:let|const|var|function|class|return|if|else|for|while)\b/g;
let code = "const x = 10; if (x > 5) return true; else return false;";
console.log(code.match(jsKeyword));
Output:
[ 'const', 'if', 'return', 'else', 'return' ]
The \b word boundaries prevent partial matches like matching "let" inside "letter" or "class" inside "classical".
Matching Multi-Word Patterns
let greeting = /^(?:good morning|good afternoon|good evening|hello|hi)\b/i;
console.log(greeting.test("Good morning, everyone!")); // true
console.log(greeting.test("good afternoon")); // true
console.log(greeting.test("Hello there")); // true
console.log(greeting.test("Hey what's up")); // false
Output:
true
true
true
false
This shows that alternatives can be multi-word phrases, not just single words.
Alternation Order Matters
The regex engine tries alternatives from left to right and returns the first match it finds. This means the order of your alternatives can affect the result.
Shorter Alternative First: Potential Problem
let pattern = /java|javascript/g;
let text = "I love javascript and java";
console.log(text.match(pattern));
Output:
[ 'java', 'java' ]
The engine finds "java" at the start of "javascript" and stops looking. It never reaches the "javascript" alternative because "java" already succeeded.
Longer Alternative First: The Fix
let pattern = /javascript|java/g;
let text = "I love javascript and java";
console.log(text.match(pattern));
Output:
[ 'javascript', 'java' ]
When alternatives overlap (one is a prefix of another), place the longer alternative first. The regex engine returns the first match, so java|javascript will never match "javascript" as a whole.
Another Overlapping Example
// WRONG order: "do" matches before "done" gets a chance
let bad = /\b(?:do|done|doing)\b/g;
console.log("I am done doing this, let me do it.".match(bad));
Output:
[ 'done', 'doing', 'do' ]
Actually, in this case word boundaries save us because "done" cannot match as just "do" (the \b after "do" would not be at position 2 of "done"). But without boundaries:
let bad = /do|done|doing/g;
console.log("done doing do".match(bad));
Output:
[ 'do', 'do', 'do' ]
The engine matches "do" at the start of each word and never tries the longer alternatives. Fix by ordering longest first:
let good = /doing|done|do/g;
console.log("done doing do".match(good));
Output:
[ 'done', 'doing', 'do' ]
Nested Alternation
You can nest alternation groups inside each other for complex pattern structures:
// Match color descriptions: "light red", "dark blue", "red", "blue"
let colorPattern = /(?:(?:light|dark) )?(?:red|green|blue|yellow)/g;
let text = "I like light red, dark blue, green, and light yellow walls.";
console.log(text.match(colorPattern));
Output:
[ 'light red', 'dark blue', 'green', 'light yellow' ]
Breaking this down:
| Part | Meaning |
|---|---|
(?:(?:light|dark) )? | Optionally match "light " or "dark " |
(?:red|green|blue|yellow) | Match one of the color names |
Building Complex Patterns Step by Step
When patterns get complex, build them incrementally:
// Step 1: Basic email-like pattern
// username@domain.tld
// Step 2: Define parts
let username = /[\w.+-]+/;
let domain = /[\w-]+/;
let tld = /(?:com|org|net|edu|io|dev)/;
// Step 3: Combine into a source string
let emailPattern = new RegExp(
`${username.source}@${domain.source}\\.${tld.source}`,
'g'
);
let text = "Contact john@example.com or jane@company.org for info.";
console.log(text.match(emailPattern));
Output:
[ 'john@example.com', 'jane@company.org' ]
Alternation vs Character Classes
A common question is when to use alternation | and when to use a character class [...]. The rule is straightforward:
| Feature | Character Class [...] | Alternation | |
|---|---|---|
| Matches | Single characters | Strings of any length |
| Example | [aeiou] matches one vowel | cat|dog matches a word |
| Performance | Faster for single chars | Slightly slower |
| Use when | Alternatives are single characters | Alternatives are multi-character |
// BAD: alternation for single characters (works but wasteful)
let vowelBad = /a|e|i|o|u/;
// GOOD: character class for single characters
let vowelGood = /[aeiou]/;
// Both produce the same result:
console.log("hello".match(vowelBad)); // [ 'e' ]
console.log("hello".match(vowelGood)); // [ 'e' ]
Output:
[ 'e' ]
[ 'e' ]
Use character classes [...] when each alternative is a single character. Use alternation | when alternatives are multi-character strings or complex sub-patterns. Character classes are optimized by the engine and perform better for single-character matching.
However, sometimes single-character alternation is necessary when combined with longer alternatives:
// This makes sense: mixing single chars with multi-char alternatives
let lineBreak = /\r\n|\r|\n/;
Here, \r\n (two characters) must be tried first, followed by \r and \n individually. A character class cannot express the two-character sequence.
Combining Alternation with Anchors and Quantifiers
With Anchors
// Match the full string as one of these values
let status = /^(?:active|inactive|pending|archived)$/;
console.log(status.test("active")); // true
console.log(status.test("inactive")); // true
console.log(status.test("actively")); // false (extra chars)
console.log(status.test("not active")); // false (extra chars)
Output:
true
true
false
false
Without the group, the anchors would only apply to the first and last alternative:
// WRONG: ^ applies to "active", $ applies to "archived"
let bad = /^active|inactive|pending|archived$/;
// Reads as:
// ^active (starts with "active")
// inactive (contains "inactive" anywhere)
// pending (contains "pending" anywhere)
// archived$ (ends with "archived")
console.log(bad.test("xxinactivexx")); // true (not what you want!)
Output:
true
With Quantifiers
// Match one or more fruits separated by commas
let fruitList = /^(?:apple|banana|cherry)(?:,\s*(?:apple|banana|cherry))*$/;
console.log(fruitList.test("apple")); // true
console.log(fruitList.test("apple, banana")); // true
console.log(fruitList.test("apple, banana, cherry")); // true
console.log(fruitList.test("apple, grape")); // false
Output:
true
true
true
false
Using Alternation with replace()
Alternation is very useful in replacement operations:
// Normalize different newline styles to \n
let text = "line1\r\nline2\rline3\nline4";
let normalized = text.replace(/\r\n|\r|\n/g, "\n");
console.log(JSON.stringify(normalized));
Output:
"line1\nline2\nline3\nline4"
Note the order: \r\n comes first to prevent \r from matching the \r in \r\n independently.
Replacing Multiple Patterns with a Map
let replacements = {
"JS": "JavaScript",
"TS": "TypeScript",
"PY": "Python"
};
// Build pattern from keys
let pattern = new RegExp(`\\b(?:${Object.keys(replacements).join("|")})\\b`, "g");
let text = "I code in JS and TS, but sometimes PY too.";
let result = text.replace(pattern, match => replacements[match]);
console.log(result);
Output:
I code in JavaScript and TypeScript, but sometimes Python too.
This is a powerful pattern: build the regex dynamically from data, then use a callback function for context-aware replacements.
Common Mistakes
Mistake 1: Forgetting to Group Around Alternation
// Intention: match "Mr." or "Mrs." followed by a name
// WRONG
let bad = /Mr|Mrs\.\s+\w+/;
// This means: "Mr" or "Mrs. followed by space and word"
console.log(bad.test("Mr")); // true (just "Mr" alone)
console.log(bad.test("Mrs. Smith")); // true
console.log(bad.test("Mr. Smith")); // true (matches only "Mr" part)
let match = "Mr. Smith".match(bad);
console.log(match[0]); // "Mr" (only matched "Mr", not "Mr. Smith")
Output:
true
true
true
Mr
The fix is to group the alternation:
// CORRECT
let good = /(?:Mr|Mrs)\.\s+\w+/;
let match = "Mr. Smith".match(good);
console.log(match[0]); // "Mr. Smith"
Output:
Mr. Smith
Mistake 2: Empty Alternatives
An empty alternative matches the empty string, which is almost never what you want:
// Accidental empty alternative from trailing |
let bad = /cat|dog|/;
// The third alternative is empty (matches empty string everywhere9
console.log(bad.test("")); // true!
console.log(bad.test("anything")); // true (empty string matches at position 09
Output:
true
true
Make sure there are no leading, trailing, or double | characters:
// CORRECT
let good = /cat|dog/;
console.log(good.test("")); // false
console.log(good.test("anything")); // false
Output:
false
false
Mistake 3: Not Escaping Special Characters in Alternatives
// WRONG: the dot matches any character
let bad = /\.js|\.ts|\.py/g;
// Actually this IS correct because the dots are escaped.
// But what about this:
let reallyBad = /.js|.ts|.py/g;
let text = "main.js, 4js, xts";
console.log(text.match(reallyBad));
Output:
[ '.js', '4js', 'xts' ]
The unescaped dots match any character, so "4js" and "xts" incorrectly match. Always escape literal special characters:
let good = /\.js|\.ts|\.py/g;
let text = "main.js, 4js, xts";
console.log(text.match(good));
Output:
[ '.js' ]
Mistake 4: Unnecessary Alternation
Sometimes developers use alternation when a simpler construct works:
// Unnecessarily verbose
let bad = /true|false/;
// Simpler and equivalent for boolean checking
let good = /true|false/; // Actually, this is fine
// But for single characters:
// BAD
let vowelBad = /a|e|i|o|u/;
// GOOD
let vowelGood = /[aeiou]/;
// And for optional parts:
// VERBOSE
let protocolBad = /http|https/;
// CONCISE
let protocolGood = /https?/;
Look for opportunities to simplify with ? (optional), */+ (repetition), or [...] (character classes) before reaching for alternation.
Performance Notes
The regex engine evaluates alternatives left to right, stopping at the first match. This means:
- Place the most common alternative first for faster average matching
- Place longer alternatives before shorter overlapping ones for correct matching
- Prefer character classes over single-character alternation for speed
- Anchor patterns when possible to avoid scanning the entire string
// If "com" is most common, put it first
let tld = /\.(?:com|org|net|edu|gov)/;
// Anchored version avoids unnecessary scanning
let tldAnchored = /\.(?:com|org|net|edu|gov)$/;
For very long lists of literal alternatives, consider whether a Set or includes() check might be simpler and faster than a regex:
// Regex approach (fine for moderate lists)
let valid = /^(?:apple|banana|cherry|date|elderberry|fig|grape)$/;
// Set approach (better for long lists or dynamic data)
let validSet = new Set(["apple", "banana", "cherry", "date", "elderberry", "fig", "grape"]);
let fruit = "cherry";
console.log(valid.test(fruit)); // true
console.log(validSet.has(fruit)); // true (clearer and faster for large sets)
Summary
| Concept | Syntax | Example |
|---|---|---|
| Basic alternation | a|b | /cat|dog/ matches "cat" or "dog" |
| Grouped alternation | (?:a|b) | /gray (?:cat|dog)/ matches "gray cat" or "gray dog" |
| Capturing alternation | (a|b) | /(cat|dog)/ captures which matched |
| Multiple alternatives | a|b|c|d | /red|green|blue/ |
Key takeaways:
- The
|operator splits the entire pattern into alternatives unless you use parentheses to limit its scope - Always group alternation with
(?:...)or(...)when it is part of a larger pattern - Use non-capturing groups
(?:...)when you do not need to extract the matched alternative - Place longer alternatives first when one is a prefix of another
- Prefer character classes
[abc]over alternationa|b|cfor single-character alternatives - Watch out for empty alternatives from stray
|characters - Alternation order matters: the engine stops at the first successful match, reading left to right
Alternation is fundamental to regex, appearing in nearly every non-trivial pattern. Mastering its scoping rules through proper grouping is the key to writing patterns that behave exactly as you intend.