Parse before you format
A string of national digits is ambiguous without a default country. Pass an explicit ISO country when the input does not start with a country calling code. If the user enters an international value beginning with +, let the parser derive the numbering region from maintained metadata.
Google's libphonenumber provides JavaScript support for parsing, formatting and validating international numbers. Choose a maintained package or binding whose metadata update process fits your application.
A useful formatter does not start by adding spaces. It first decides what the input means. For example, 020 7946 0018 can be interpreted as a UK national number only when the selected country is GB, while +44 20 7946 0018 carries its own calling code. Those two inputs may produce the same canonical value, but the path to that value is different and should be visible in your code.
Keep canonical and display values separate
const parsed = parsePhoneNumber(input, selectedCountry);
const storedValue = parsed.number; // +442079460018
const localLabel = parsed.formatNational(); // 020 7946 0018
const globalLabel = parsed.formatInternational();The exact API depends on the library you choose, but the data flow should remain the same: parse with context, validate the claim you need, store E.164 and format at the presentation edge. Try the E.164 formatter to compare the output forms.
A minimal formatting pipeline
Keep the pipeline boring and explicit. It should accept raw input and a country hint, normalize presentation characters, parse with metadata, return a canonical value and a display label, then preserve the original input when parsing fails. Avoid one helper that silently guesses everything; formatting mistakes become much harder to debug when parsing, validation and display are hidden behind one boolean.
function normalizePhoneInput(rawInput, country) {
const input = rawInput.trim().replace(/[\u00a0\u2007\u202f]/g, " ");
const parsed = parsePhoneNumberFromString(input, country);
if (!parsed || !parsed.isPossible()) {
return { ok: false, reason: "not_possible", original: rawInput };
}
return {
ok: true,
e164: parsed.number,
national: parsed.formatNational(),
international: parsed.formatInternational(),
country: parsed.country || country
};
}This shape is intentionally small. It leaves business rules outside the formatter: whether your product accepts mobile numbers only, whether the value must be verified, and whether the selected country is enabled belong in separate checks with separate error messages.
Use as-you-type formatting carefully
Incremental formatting can help users read long inputs, but it must preserve cursor position, pasted values and country changes. Do not reject an incomplete number while the user is still typing. Show a hard error on submission or when the input is clearly impossible.
Keep the country selector accessible by keyboard and text label. A flag alone does not communicate a numbering plan and cannot distinguish every territory.
Design useful error states
Users need to know what to fix, not whether a formatter feels disappointed. Return a small set of stable reasons such as missing country, impossible length, unsupported country, unsupported line type and verification required. Do not mark an unverified number as invalid if it parses correctly; ownership is a later step.
| Error | Meaning | Better message |
|---|---|---|
missing_country | The input has no calling code and no selected region. | Choose a country so we can read this national number. |
not_possible | The digit count does not fit the country metadata. | Check the number length and country selection. |
unsupported_region | Your product does not support the parsed country. | This country is not supported for this workflow yet. |
verification_required | The value parses, but the user has not proved control. | We need to send a verification code before using this number. |
Repeat important checks on the server
Client-side formatting improves usability; it is not a trust boundary. Parse and normalize again on the server before deduplication, storage or delivery. Pin library versions, update metadata deliberately and run regression fixtures for every supported region after an upgrade.
The server should store the canonical value, the selected or parsed country, the library metadata version and any verification status. That makes future changes auditable: if a metadata update changes a classification, you can identify whether an existing record was accepted under older rules instead of guessing from raw strings.
Store phone data as identifiers
Use text fields rather than numbers. A phone number is an identifier, not a value for arithmetic. The plus sign, leading zeros in some contexts and extension data all have meaning that numeric storage can destroy. Store extensions, consent timestamps, verification state and preferred display region in separate fields instead of appending everything to one string.
{
"phoneE164": "+442079460018",
"phoneRegion": "GB",
"phoneVerifiedAt": null,
"phoneMetadataVersion": "reviewed-2026-08"
}For database tradeoffs, see the phone-number database design guide.
JavaScript test cases to include
- National and international forms for each supported country.
- Pasted spaces, parentheses, hyphens and non-breaking spaces.
- A trunk prefix that must be removed and a significant zero that must remain.
- One digit below and above valid boundaries.
- Country changes after digits have already been typed.
- An extension stored separately from the E.164 value.
- Shared calling-code plans such as
+1, where country cannot be inferred from the first digit alone. - URLs, analytics events and logs that must not expose the full phone value.
Build the matrix with the phone-number testing checklist and use the bulk fixture generator for repeatable positive cases. Negative cases should be written by hand so each one fails for a single known reason.
When JavaScript formatting is not enough
Do not use a formatter as a fraud check, carrier lookup, contact-enrichment tool or proof of ownership. Formatting can help the interface understand the string a user typed. It cannot prove assignment, active service, SMS capability or user consent. Those jobs require current provider data, delivery attempts, consent-based verification and privacy controls that live outside a display formatter.
Code review questions for JavaScript formatters
Ask whether the formatter preserves the original input while editing, whether it can handle pasted punctuation and non-breaking spaces, and whether it fails gracefully when country context is missing. Confirm that the same logic is repeated or verified on the server before storage. Client-side formatting is a usability layer, not a security boundary.
Keep analytics events limited to safe operational facts such as selected country, success state and error category. Do not send full phone strings as event labels, URL parameters or exception messages.
How this guide connects to the toolkit
This page is written as an implementation reference, not a generic glossary. Use it with the interactive tools when you need to turn an idea into a testable behavior: generate a fixture, format it, validate the structural claim, then record the country and expected result beside your product test. Keeping the explanation and tool output connected helps future reviewers understand why a number was accepted or rejected.
If you copy an example into your own documentation, also copy the safety context. A phone string without its country, source note and limitation can look like a contactable number. The surrounding guidance is part of the value of the fixture.