Why does /^cat|dog$/.test('hotdog') return true in JavaScript, and how do you write the pattern you probably meant?
answer
- one operator binds loosest of all
- the split is wider than it looks
- anchors ended up on one branch only
- brackets around the whole expression
- non-capturing group restores the intent
basics
~20 sAlternation has the lowest precedence in a regex, so the pattern splits into two whole branches: ^cat, or dog$. 'hotdog' ends with dog, satisfying the second branch. Group the branches — /^(?:cat|dog)$/ — to make the anchors apply to both.
solid answer
~40 sThe `|` operator binds more loosely than anything else in a pattern, so it splits the entire enclosing expression, not just the tokens next to it. `/^cat|dog$/` therefore means "starts with cat" OR "ends with dog", and `'hotdog'` satisfies the second branch. The fix is to scope the alternation explicitly: `/^(?:cat|dog)$/` puts both anchors outside a group that holds only the two words. This is the single most common regex reading error, and it shows up wherever a pattern mixes alternation with anchors or with surrounding literal text — `/^ab|cd$/`, `/prefix-a|b/`, and so on. Use a non-capturing group unless you actually want the chosen branch captured; either way, the parentheses are what make the intent unambiguous.
code
javascript · 3 linesconsole.log(/^cat|dog$/.test('hotdog')); // true — parses as (^cat)|(dog$)
console.log(/^(?:cat|dog)$/.test('hotdog')); // false
console.log(/^(?:cat|dog)$/.test('dog')); // truego deeper
Be able to say that | splits the whole pattern into alternatives and that grouping the branches is how you make anchors apply to all of them.
State the precedence order — atoms, then quantifiers, then concatenation, then alternation — and correct a broken pattern live, including the prefix-branch ordering trap.
Bring the review instinct: patterns mixing anchors with alternation are a standing defect source, and \b silently misbehaves on non-ASCII input, so you check both when a matcher misses records.
Own the guardrails — shared patterns get tests with a negative case per branch, and you decide when input should be normalised or structured so that pattern matching stops carrying so much weight.
## Precedence, stated plainly Regex syntax has an operator precedence just like arithmetic does, and `|` sits at the bottom of it. From tightest to loosest: 1. Escaped characters, character classes, groups — the atoms. 2. Quantifiers (`*`, `+`, `?`, `{n,m}`) — bind to the single preceding atom. 3. Concatenation — a sequence of atoms in a row. 4. Alternation `|` — splits everything else. So `/^cat|dog$/` parses as `(^cat)|(dog$)`, not as `^(cat|dog)$`. The two branches are complete, independent alternatives; each is tried in order at each starting position, and the first that succeeds wins. ```js console.log(/^cat|dog$/.test('hotdog')); // true — branch 2: dog$ console.log(/^cat|dog$/.test('catalog')); // true — branch 1: ^cat console.log(/^(?:cat|dog)$/.test('hotdog'));// false console.log(/^(?:cat|dog)$/.test('dog')); // true ``` The same mistake with plain text instead of anchors is just as common: `/error: timeout|refused/` means "the literal `error: timeout`" or "the bare word `refused`", and will happily match a line that says `connection refused` with no `error:` prefix at all. ## Fixing it Wrap the alternatives in a group so the alternation cannot reach past it: `/^(?:cat|dog)$/`, `/error: (?:timeout|refused)/`. Use a **non-capturing** group unless you want to know which branch matched; if you do want that, a capturing or named group is the natural place to record it. Grouping is the fix in every case — there is no flag or setting that changes `|` precedence. A related trap is quantifier scope, which is the same class of error one level up: `/ab+/` repeats only the `b`, and `/(?:ab)+/` repeats the pair. Quantifiers bind to one atom; parentheses are how you make several tokens into one atom. ## Anchors and boundaries are assertions, not characters Because this question is usually really about anchors, know what they assert. `^` asserts the start of input, `$` the end of input — or, with the `m` flag, the start and end of each line. Neither consumes a character; both are zero-width, which is why `/^$/` matches the empty string. `\b` is the word-boundary assertion: it succeeds at a position where exactly one side is a "word character", where word character means `[A-Za-z0-9_]` — ASCII only. That last detail bites: ```js console.log(/\bcat\b/.test('concatenate')); // false — no boundary either side console.log(/\bcat\b/.test('the cat sat')); // true console.log(/\bcafé\b/.test('café')); // false ``` The third line surprises people. `é` is not in `\w`, so between `é` and the end of the string there is no word/non-word transition and therefore no boundary. For non-ASCII text, `\b` is simply the wrong tool; you need explicit lookarounds over the character classes you actually mean. `\B` is the negation — it asserts a position that is *not* a word boundary. ## Branch order matters Alternation is ordered and non-greedy across branches: the engine tries the leftmost branch first and, if the rest of the pattern can complete, keeps it. It does not prefer the longest alternative. ```js console.log('football'.match(/foot|football/)[0]); // 'foot' console.log('football'.match(/football|foot/)[0]); // 'football' ``` So when one alternative is a prefix of another, put the longer one first — otherwise the shorter always wins. This is a routine bug in keyword and unit patterns (`/m|min|month/` will never reach `month`). ## Reading patterns defensively The habit worth building: whenever you see a `|`, mentally draw brackets around the whole enclosing expression and ask whether that is really the split you want. If the answer is no — and it usually is when anchors, prefixes, or suffixes are present — add a group. Being able to state "alternation has the lowest precedence, so it splits the entire enclosing expression" and then correct a broken pattern on the spot is exactly what the interviewer is checking.
- Given the pattern /foot|football/, what does 'football'.match(...)[0] return, and why?`'foot'`. Alternation is ordered, not longest-match: the engine tries the leftmost branch first and keeps it as soon as the rest of the pattern can complete — and here nothing follows. Put the longer alternative first, `/football|foot/`, when one branch is a prefix of another.
- Why does /\bcafé\b/ fail to match the string 'café'?`\b` asserts a transition between a `\w` character and a non-`\w` one, and `\w` is ASCII-only: `[A-Za-z0-9_]`. `é` is not a word character, so at the end of the string there is no transition and the trailing `\b` fails. For non-ASCII text use explicit lookarounds instead.
- Should you use a capturing or a non-capturing group to scope an alternation?Non-capturing by default — you are grouping for precedence, not to read a value, and a stray capture renumbers everything after it. Reach for a capturing or named group only when the calling code genuinely needs to know which branch matched.
saying these in an interview costs you the question
- Thinks | binds only to the adjacent characters
- Believes alternation picks the longest matching branch
- Assumes an anchor at either end applies to all branches
- Says \b works on accented or non-Latin letters
- Adds a capturing group purely to fix precedence