How to Validate International Phone Numbers in JavaScript (2026)
Phone number validation sounds simple until you go international. US numbers have 10 digits, UK mobiles have 11, German numbers range from 7 to 12, and India uses a completely different prefix system. A basic regex that works for one country breaks on all the others.
This guide covers four approaches, from simple to production-grade, with working code for each.
The Problem with Simple Regex
Most developers start here:
// DON'T use this in production
const isPhone = /^\d{10}$/.test(input);
// Misses: +44 7911 123456, 0049 170 1234567, +91 98765 43210
This only matches exactly 10 digits. It rejects every valid international number, numbers with country codes, parentheses, dashes, or spaces. It also accepts invalid 10-digit strings that aren't real phone numbers.
Approach 1: Country-Specific Regex Patterns
Instead of one regex, use a separate pattern per country:
const patterns = {
US: /^1?([2-9]\d{2}[2-9]\d{6})$/,
GB: /^(?:44)?0?(7\d{9}|[1-9]\d{8,9})$/,
DE: /^(?:49)?0?([1-9]\d{4,13})$/,
FR: /^(?:33)?0?([1-9]\d{8})$/,
IN: /^(?:91)?0?([6-9]\d{9})$/,
};
function validatePhone(digits, country) {
// Strip non-digits
const cleaned = digits.replace(/[\s\-\(\)\.\+]/g, '');
const pattern = patterns[country];
if (!pattern) return { valid: false, reason: 'Unsupported country' };
return { valid: pattern.test(cleaned) };
}
Pros: Zero dependencies, fast, works offline.
Cons: You need to maintain regex for every country. You also need the caller to specify the country.
Approach 2: Google's libphonenumber
Google maintains libphonenumber, originally built for Android. The JavaScript port is google-libphonenumber:
npm install google-libphonenumber
const { PhoneNumberUtil, PhoneNumberFormat } = require('google-libphonenumber');
const phoneUtil = PhoneNumberUtil.getInstance();
function validate(input, countryCode) {
try {
const number = phoneUtil.parse(input, countryCode);
return {
valid: phoneUtil.isValidNumber(number),
formatted: phoneUtil.format(number, PhoneNumberFormat.E164),
type: phoneUtil.getNumberType(number), // 0=FIXED, 1=MOBILE, etc.
country: phoneUtil.getRegionCodeForNumber(number),
};
} catch (e) {
return { valid: false, reason: e.message };
}
}
Pros: Most accurate. Handles every country, number type, and format.
Cons: The bundle is ~1.2 MB. That's massive for a frontend app and significant for serverless functions where cold start time matters.
Approach 3: API-Based Validation
Offload the work to an API. This keeps your bundle size at zero and gets you extra data like location and line type:
async function validatePhone(input) {
const res = await fetch(
`https://datacheck.dev/api/validate?input=${encodeURIComponent(input)}&type=phone`
);
return res.json();
}
// Returns:
// {
// valid: true,
// formatted: "+1 4155551234",
// country: "US",
// details: {
// type: "mobile",
// area_code: "415",
// location: "San Francisco, CA"
// }
// }
Pros: Zero bundle size. Returns enriched data (location, line type). Always up to date.
Cons: Requires network call. Depends on external service uptime.
Try it free →
Approach 4: Hybrid (Recommended)
The production sweet spot: validate format client-side for instant feedback, then verify server-side via API before saving:
// Client-side: quick format check
function quickCheck(input) {
const digits = input.replace(/\D/g, '');
if (digits.length < 7 || digits.length > 15) {
return { valid: false, reason: 'Phone must be 7-15 digits' };
}
return { valid: true };
}
// Server-side: full validation before saving
async function fullValidation(input) {
const res = await fetch(
`https://datacheck.dev/api/validate?input=${encodeURIComponent(input)}&type=phone`
);
const data = await res.json();
if (!data.valid) throw new Error(data.reason);
return data;
}
This gives users instant feedback on typos while catching all edge cases on the backend.
Comparison Table
| Approach | Accuracy | Bundle Size | Countries | Extra Data |
|---|---|---|---|---|
| Simple regex | Low | 0 KB | 1 | No |
| Country patterns | Medium | ~2 KB | 5-30 | No |
| libphonenumber | High | ~1.2 MB | 250+ | Type only |
| API (DataCheck) | High | 0 KB | 30+ | Type, location, area code |
| Hybrid | High | ~1 KB | 30+ | Type, location, area code |
Common Pitfalls
1. Stripping the + sign too early
The + prefix indicates an international format. If you strip it before parsing, +44 becomes 44, which could be confused with a domestic number starting with 4.
2. Assuming all countries have the same length
US numbers are always 10 digits. German numbers range from 7 to 12 digits. Chinese mobile numbers are 11 digits. Never hardcode length.
3. Ignoring leading zeros
In the UK, 07911 123456 is valid locally but the leading 0 is dropped in international format: +44 7911 123456. Your validator needs to handle both.
4. Not returning formatted output
Always store the E.164 formatted version (+14155551234). This is the universal format that SMS gateways, Twilio, and phone APIs expect.
Wrapping Up
For hobby projects, country-specific regex works fine. For production apps with international users, use either libphonenumber (if bundle size isn't a concern) or an API like DataCheck (if you want zero dependencies and enriched data).
The hybrid approach gives you the best of both: instant client-side feedback and accurate server-side validation with location data.
Get your free API key →