[a-z] ranges: code point order and the A-z bug
A range in a character class covers every code point from the first character to the last, inclusive. It knows nothing about alphabets, so the boundaries are numeric, not linguistic.
At a glance
- Syntax
- [a-z]
- Ordering
- by code point, and the start must not be above the end
- Both cases
- [A-Za-z], written as two ranges
- [A-z]
- also matches [ \ ] ^ _ and the backtick, which sit between Z and a
- [a-Z]
- a SyntaxError: range out of order in character class
- Literal hyphen
- put it first or last in the class, or escape it
In detail
A range is arithmetic on code points. [a-z] is every code point from 97 to 122 inclusive, and the engine has no idea that those happen to be letters. That is why a range cannot be made accent aware by any flag, and why the only reliable way to widen it is to switch to a Unicode property escape.
The classic bug is [A-z], written by someone who wanted both cases and did not want to type two ranges. Capital A is 65 and lowercase z is 122, and the gap between them contains six punctuation characters: the opening bracket, the backslash, the closing bracket, the caret, the underscore and the backtick. So /[A-z]+/ matches the whole of "A_z" and a validator built on it accepts a backslash.
Getting the ends the wrong way round is at least loud. [a-Z] throws a SyntaxError about a range out of order, because 97 is greater than 90. That failure happens at compile time, which is the one thing in this area that behaves helpfully.
The hyphen itself is only a range operator between two other characters. In first position, in last position, or escaped, it is an ordinary hyphen, which is how [0-9-] manages to mean digits and hyphens. Putting it last is the conventional spelling because it survives someone later adding another item to the class.
Examples
/[A-z]+/.exec("A_z")[0]; // "A_z", the underscore is inside the range
/[A-Za-z]+/.exec("A_z")[0]; // "A", two explicit ranges instead
new RegExp("[a-Z]"); // SyntaxError: Range out of order in character class"2026-03-08".match(/[0-9-]+/)[0]; // "2026-03-08"The hyphen is last in the class, so it is an ordinary character rather than a range operator.
Things that catch people out
- [A-z] is not a shorthand for both cases. It also accepts the backslash, the caret, the underscore, the backtick and both square brackets.
- Ranges know nothing about accents. [a-z] does not match é, and no flag will change that, so a Unicode property escape is the only real fix.
- A hyphen in the middle of a class silently creates a range. [a-z-0-9] is not what it looks like, and a mistyped class of this shape can accept a large block of punctuation.
Try it: Regex Tester
Type a pattern and a test string, watch every match highlight as you go, read a plain English breakdown of each token, and check the numbered and named groups it captured.
Regex Tester
LOCAL · Browser-OnlyTest and debug regular expressions with live matching and highlights
Related Regex syntax
All regex syntax →[abc]: character sets and what loses its meaning inside
A bracket set matches one character from a list. Most metacharacters go literal inside it, and only four keep any special meaning at all.
The i flag: case insensitive matching
How the i flag folds case in literals and ranges, what it deliberately leaves alone, and the two non-ASCII characters it starts matching once u is on.
\p{...}: Unicode property escapes, and the flag they need
Property escapes match by Unicode category or script instead of by code point range. Without the u flag they compile into something else entirely.
\b: word boundaries, underscores and accented letters
\b is a zero width assertion defined entirely in terms of \w, which is why user_name has no internal boundary and café has none at its end.
\B: the non word boundary and what it is good for
\B asserts that a position is not a word boundary, which turns it into an inside a word search. It also never matches at the edges of a word.
The * quantifier: zero or more, including nothing at all
The star quantifier succeeds even when it matches nothing, which is why a global match against it returns an empty string at almost every position.
Tools for this
- Regex Tester Free online regex tester - test and debug regular expressions with live matching and highlights
- Regex Playground Regex playground with live match highlighting, named groups, find and replace, and shareable links