How to Validate International Phone Numbers in JavaScript (2026)

JavaScript Tutorial February 2026 · 9 min read

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.

DataCheck validates phone numbers for 30+ countries with area code lookup.
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

ApproachAccuracyBundle SizeCountriesExtra Data
Simple regexLow0 KB1No
Country patternsMedium~2 KB5-30No
libphonenumberHigh~1.2 MB250+Type only
API (DataCheck)High0 KB30+Type, location, area code
HybridHigh~1 KB30+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.

Validate phone numbers, credit cards, IBANs, VAT IDs, and postal codes with one API.
Get your free API key →