Regex for phone numbers, and why strict ones lose customers
Phone number patterns break on formatting nobody anticipated. Stripping every non-digit first and then counting digits is more robust than any single pattern.
At a glance
- Built in preset
- Phone (US)
- Preset pattern
- (?:\+1[-.]?)?\(?\d{3}\)?[-.]?\d{3}[-.]?\d{4}
- Preset flags
- g
- Separators allowed
- a hyphen or a full stop only, never a space
- Consequence
- (555) 123-4567 does not match, because of the space after the bracket
- More robust approach
- strip non-digits, then check the digit count
In detail
The preset is a reasonable sketch of a North American number: an optional country code, three digits with optional brackets, then three, then four, with a hyphen or a full stop permitted between the groups. It handles +1-800-555-0199 and 555.867.5309 exactly as you would expect.
It also demonstrates the whole problem, because it does not match (555) 123-4567. The separator class is a hyphen or a full stop and contains no space, so once the closing bracket is followed by a space the attempt fails, and there is no shorter run of digits in the string long enough to match instead. That form appears in the preset's own sample text, which is the clearest possible illustration of how easily a phone pattern misses a format its author had in mind.
Widening the class fixes that case and invites the next one. Real input contains non breaking spaces, en spaces from a word processor, slashes, extension suffixes, and country codes written half a dozen ways. Every widening also loosens the check, and a pattern permissive enough for real users stops rejecting much of anything.
The approach that survives contact with users is to normalise first. Strip every non-digit, then decide based on the digit count and any leading country code, which reduces a formatting problem to an arithmetic one. If the number has to be genuinely valid rather than merely plausible, that is a job for a phone number library with per country rules, not for a pattern.
Examples
const phone = /(?:\+1[-.]?)?\(?\d{3}\)?[-.]?\d{3}[-.]?\d{4}/g;
"Call (555) 123-4567 or +1-800-555-0199 or 555.867.5309 for support."
.match(phone);
// ["+1-800-555-0199", "555.867.5309"]The parenthesised number is missed entirely, because the separator class allows a hyphen or a full stop and not a space.
function normalise(s) {
return s.replace(/\D/g, "");
}
normalise("(555) 123-4567"); // "5551234567", 10 digits
normalise("+1 (555) 123-4567"); // "15551234567", 11 with the country code
/^1?\d{10}$/.test(normalise("(555) 123-4567")); // trueThings that catch people out
- The built in preset does not match (555) 123-4567, because it allows a hyphen or a full stop between groups but not a space.
- Stripping non-digits also strips a leading plus sign, so a country code becomes indistinguishable from a leading digit unless you check for it first.
- A pattern loose enough to accept every real formatting habit no longer rejects much of anything, which makes it worth asking what the check was for.
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 →Regex for email addresses: what can and cannot be validated
No regular expression fully validates an email address. Here is what the built in preset accepts, what it rejects, and the only check that settles it.
The ? quantifier: optional, and its two other jobs
A question mark after a token makes it optional. The same character also marks a lazy quantifier and opens a special group, which is why patterns look dense.
(?:...): grouping without creating a capture
A non capturing group applies quantifiers and alternation without taking a number, which is what keeps $1 pointing at the same thing after an edit.
Regex to trim whitespace, collapse runs and drop blank lines
There is no preset for this one. Four short patterns cover trimming, collapsing, per line trimming and removing blank lines, and each has a different failure.
Regex for HTML tags: what the preset matches and what breaks it
The tag preset is fine for finding tags in a known snippet and wrong for parsing a document. A greater than sign inside an attribute value is all it takes.
Regex for numbers: integers, decimals and negative signs
The integer preset splits 3.14 into two matches and the float preset ignores whole numbers. Here is the combined pattern and what it still leaves out.
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