javascript
38 lines · 6 steps
Normalizing phone numbers with typed errors
A validator that parses free-form phone input and throws distinct error codes at each failure stage before returning clean formats.
Explained by
highlit
1import { parsePhoneNumberFromString } from 'libphonenumber-js';
2
3export class PhoneValidationError extends Error {
4 constructor(message, code) {
5 super(message);
6 this.name = 'PhoneValidationError';
7 this.code = code;
8 }
9}
10
11export function normalizePhoneNumber(input, defaultCountry = 'US') {
12 if (typeof input !== 'string' || input.trim() === '') {
13 throw new PhoneValidationError('Phone number is required', 'EMPTY');
14 }
15
16 const parsed = parsePhoneNumberFromString(input.trim(), defaultCountry);
17
18 if (!parsed) {
19 throw new PhoneValidationError(
20 `Could not parse "${input}"`,
21 'UNPARSEABLE',
22 );
23 }
24
25 if (!parsed.isValid()) {
26 throw new PhoneValidationError(
27 `"${input}" is not a valid number for ${parsed.country ?? defaultCountry}`,
28 'INVALID',
29 );
30 }
31
32 return {
33 e164: parsed.number,
34 national: parsed.formatNational(),
35 country: parsed.country,
36 type: parsed.getType(),
37 };
38}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1A custom Error subclass with a code field lets callers branch on failure type instead of parsing message strings.
- 2Guarding each distinct failure separately produces precise diagnostics rather than one vague error.
- 3Returning a normalized object of multiple formats frees callers from re-formatting the same value.
Related explainers
javascript
import { useDeferredValue, useMemo, useState } from "react"; function ProductSearch({ products }) { const [query, setQuery] = useState("");
Keeping search input snappy with useDeferredValue in React
concurrent-rendering
deferred-value
memoization
Intermediate
7 steps
rust
use axum::{ extract::{FromRef, FromRequestParts}, http::{header, request::Parts, StatusCode}, RequestPartsExt,
How a JWT extractor works in Axum
jwt
authentication
extractors
Intermediate
8 steps
typescript
import { InjectionToken, inject, Provider, isDevMode } from '@angular/core'; import { WINDOW } from './window.token'; export interface AnalyticsConfig {
Layered config with an Angular InjectionToken
dependency-injection
configuration
factory-provider
Intermediate
8 steps
javascript
const IBAN_LENGTHS = { DE: 22, FR: 27, GB: 22, ES: 24, IT: 27, NL: 18, BE: 16, CH: 21, AT: 20, PT: 25, };
How IBAN validation works in JavaScript
validation
checksum
modular-arithmetic
Intermediate
8 steps
ruby
class MailingListDeduplicator GMAIL_DOMAINS = %w[gmail.com googlemail.com].freeze def initialize(subscribers)
Deduplicating a mailing list by canonical email
deduplication
normalization
service object
Intermediate
8 steps
javascript
class ToastQueue { constructor(container, { duration = 4000, max = 3 } = {}) { this.container = container; this.duration = duration;
Building a rate-limited toast queue
queue
dom manipulation
throttling
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/normalizing-phone-numbers-with-typed-errors-explained-javascript-1864/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.