DEV Community

A A
A A

Posted on

A valid phone number is not necessarily a usable phone number

A valid phone number is not necessarily a usable phone number

Most applications treat phone number validation as a boolean:

js
if (isValidPhoneNumber(number)) {
// good to go
}

That is useful, but it answers a much narrower question than it looks like it does.

A number can be perfectly valid according to a country's numbering plan and still be the wrong kind of number for what your application is about to do.

If you are building OTPs, SMS notifications, account recovery, fraud checks or outbound calling, it helps to think of phone validation as several separate layers.

Layer 1: can the number be parsed?

The first problem is simply turning whatever the user typed into a consistent format.

These could all represent the same UK number:

07400 123456
07400123456
+44 7400 123456
0044 7400 123456
Enter fullscreen mode Exit fullscreen mode

Internally, you generally want to reduce that to E.164:

+447400123456
Enter fullscreen mode Exit fullscreen mode

Libraries such as libphonenumber-js handle most of this for you.

import parsePhoneNumber from "libphonenumber-js/max";

function normalizePhone(input, defaultCountry) {
  const phone = parsePhoneNumber(input, defaultCountry);

  if (!phone || !phone.isValid()) {
    return null;
  }

  return {
    e164: phone.number,
    country: phone.country
  };
}
Enter fullscreen mode Exit fullscreen mode

This gets you a normalized number and checks it against numbering-plan metadata.

But it still does not tell you very much about the actual line.

Layer 2: is it valid according to the numbering plan?

This is the distinction that often gets lost.

A phone-number library can usually tell you things such as:

+447400123456
Enter fullscreen mode Exit fullscreen mode
  • Does the length make sense?
  • Does the prefix fit the country's numbering plan?
  • Can it be normalized?
  • Which country does it appear to belong to?

That is static validation.

It does not mean somebody currently owns the number.

It also does not automatically mean the number can receive an SMS.

And it does not necessarily tell you which network serves it today.

For plenty of applications, static validation is enough. A contact form probably does not need much more.

An authentication system probably does.

Layer 3: what type of line is it?

Suppose two numbers both pass your validation library.

One is a mobile number.

The other is a fixed line.

From the point of view of isValid(), both may be completely legitimate phone numbers.

From the point of view of an SMS workflow, they are very different.

This is where line-type information becomes useful:

{
  "number": "+447400123456",
  "valid": true,
  "line_type": "mobile"
}
Enter fullscreen mode Exit fullscreen mode

Typical classifications include:

mobile
fixed_line
voip
toll_free
premium_rate
Enter fullscreen mode Exit fullscreen mode

The important part is not the label itself. It is what your application does with it.

For example:

switch (lookup.lineType) {
  case "mobile":
    return sendSms();

  case "fixed_line":
    return askForAnotherNumber();

  default:
    return applyYourOwnRules();
}
Enter fullscreen mode Exit fullscreen mode

That is a much better model than treating every syntactically valid number identically.

Layer 4: which network is it actually on?

There is another complication: number portability.

Someone can start with one mobile operator and later move their number to another.

The phone number does not change.

That means the prefix can tell you something about how a number was originally allocated, but it should not automatically be treated as the current carrier.

So there are really two different pieces of information:

numbering-plan information
current network information
Enter fullscreen mode Exit fullscreen mode

They come from different places.

A static library is extremely useful for the first.

Current carrier information normally requires a lookup against telecom data.

This distinction matters whenever your application makes decisions based on the serving network rather than merely formatting the number.

Validation is still not reachability

There is one final distinction worth keeping in mind.

Even after you know:

valid: true
line_type: mobile
carrier: Example Mobile
Enter fullscreen mode Exit fullscreen mode

you still have not mathematically proven that your next SMS will be delivered.

Phones can be switched off. Accounts can be suspended. Networks can reject traffic. Numbers can change state.

So I find it useful to keep these concepts separate:

syntax
    ↓
numbering-plan validity
    ↓
line type
    ↓
current carrier
    ↓
reachability / delivery
Enter fullscreen mode Exit fullscreen mode

Each layer answers a different question.

Trying to compress all of them into a single field called valid usually creates bad assumptions elsewhere in the system.

Design the API around the decision

Instead of returning this:

{
  "valid": true
}
Enter fullscreen mode Exit fullscreen mode

I prefer responses that expose why the application should make a decision:

{
  "normalized": "+447400123456",
  "valid": true,
  "country": "GB",
  "line_type": "mobile",
  "carrier": "Example Mobile",
  "ported": true
}
Enter fullscreen mode Exit fullscreen mode

Then your business logic stays explicit.

Your signup service might care about line_type.

Your routing service might care about carrier.

Your fraud system might care about portability or other risk signals.

Your address book might only care that the number was normalized successfully.

There is no universal definition of a "good phone number". It depends on what you are about to do with it.

Where I ran into this

Disclosure: I work on Carrier Reveal, which provides carrier, line-type and portability information for phone numbers.

Building around this problem made me realize that the mistake is usually not bad phone-number validation. It is asking one validation step to answer questions it was never designed to answer.

A library such as libphonenumber is very good at understanding telephone numbering plans.

Use it for that.

Then add live data only when your application actually needs to know something about the number as it exists today.

Top comments (0)