JAVASCRIPT / REGULAR EXPRESSIONS
Everyday patterns: emails, URLs, and numbers
Check emails, URLs, and numbers with small anchored patterns, then hand the survivors to new URL and Number for the real parsing.
What you will learn
- Use a short anchored email check as a typo filter, not as proof of deliverability.
- Locate URLs with a regex, then let new URL validate and split them into parts.
- Anchor numeric patterns and decide explicitly about signs, dots, and exponents.
- Predict where Number(), parseFloat(), and your regex disagree on one string.
Understanding Everyday patterns: emails, URLs, and numbers
These three formats tempt you into writing a full grammar with a regex, and email is the clearest warning. The address grammar in RFC 5322 permits quoted local parts, comments in parentheses, and folded whitespace, so a genuinely conformant pattern runs to hundreds of characters and still answers the wrong question: shape, not deliverability. ada@gmial.com is flawless syntax and lands nowhere, so treat the regex as a typo filter (one @, no spaces, a dot in the domain) and let a confirmation email do the validating.
For URLs the runtime already ships a parser the whole web agrees on, so use a regex only to locate candidates in free text and hand each candidate to new URL(). That split keeps ports, percent-encoding, IPv6 hosts, punycode, and relative references out of your pattern, and hands back protocol, hostname, pathname, and searchParams already separated. The parse doubles as the validity test, because an unparseable string throws instead of quietly matching something close.
Numbers are where the pattern and the conversion disagree in ways that leak into stored data. Number() trims whitespace, accepts 0x1f and Infinity, and turns an empty string into 0; parseFloat() reads a leading prefix and ignores the junk in 12px; your anchored pattern does exactly what you wrote and nothing more. Decide in words whether a leading +, a bare .5, an exponent, or thousands separators are legal, encode that one decision in an anchored pattern, and convert only afterwards. Locale text like 1.234,56 has to be normalized by you, since no pattern can guess whether a dot is a decimal point or a group separator.
// One @, no spaces, a dot in the domain: a typo filter, not a validator.
const emailish = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
for (const e of ['ada@example.com', 'ada@example', 'a b@example.com', 'ada+tag@mail.example.co.uk']) {
console.log(`email ${e} -> ${emailish.test(e)}`);
}
// The regex only finds a candidate; URL decides whether it is really a URL.
for (const link of ['https://example.com/docs?q=regex#top', 'example.com', 'ftp://files.example.com/x']) {
try {
const u = new URL(link);
console.log(`url ${link} -> ${u.protocol} ${u.hostname} ${u.pathname}`);
} catch {
console.log(`url ${link} -> not an absolute URL`);
}
}
// A pattern and Number() are two different definitions of a number.
const numberish = /^-?\d+(?:\.\d+)?$/;
for (const n of ['42', '-3.5', '3.', '1e3', '1,234']) {
console.log(`num ${n} -> pattern ${numberish.test(n)}, Number ${Number(n)}`);
}A regex decides whether a string is plausible and where it starts and ends; a purpose-built parser such as new URL or Number decides what it actually means.
Worked examples
Scan text for links, then parse them
A loose scanner finds URL candidates in prose and new URL turns each cleaned candidate into structured parts.
const text = 'Docs at https://example.com/a, mirror at (http://sub.example.org:8080/b?x=1).';
const scan = /\bhttps?:\/\/\S+/g;
for (const hit of text.match(scan)) {
const cleaned = hit.replace(/[.,;:)]+$/, '');
const u = new URL(cleaned);
console.log(`${cleaned} | host=${u.host} | search=${u.search || '(none)'}`);
}Example explained
Line 1\S+ runs to the next space, so each raw hit also swallows the comma or the closing paren that ends the sentence.
Line 2The replace strips only trailing sentence punctuation, characters a URL may legally contain but almost never ends with.
Line 3u.host keeps the :8080 port, where u.hostname would have dropped it.
Line 4u.search is an empty string when there is no query, which is why the || fallback prints (none).
Three definitions of a number
The same seven strings judged by an anchored numeric pattern, by Number(), and by parseFloat().
const numeric = /^[+-]?(?:\d+|\d*\.\d+)(?:[eE][+-]?\d+)?$/;
for (const s of ['12', '+0.5', '.5', '1_000', '12px', '0x1f', ' 7 ']) {
console.log(`[${s}] pattern=${numeric.test(s)} Number=${Number(s)} parseFloat=${parseFloat(s)}`);
}Example explained
Line 1The alternation \d+|\d*\.\d+ is what lets .5 through while a lone dot still fails.
Line 21_000 fails the pattern and Number, but parseFloat stops at the underscore and reports 1.
Line 3Number('0x1f') is 31 because string conversion accepts hex literals, while parseFloat reads only the leading 0.
Line 4' 7 ' fails the anchored pattern because ^ and $ do not skip spaces, whereas Number trims first.
Anchors and normalization on a form field
Why the same email pattern must be anchored for a field and why the value is cleaned before testing.
const anchored = /^[^\s@]+@[^\s@]+\.[a-z]{2,}$/i;
const unanchored = /[^\s@]+@[^\s@]+\.[a-z]{2,}/i;
const field = ' Ada@Example.COM ';
console.log('unanchored on a sentence:', unanchored.test('name <ada@example.com> here'));
console.log('anchored on a sentence:', anchored.test('name <ada@example.com> here'));
console.log('anchored on the raw field:', anchored.test(field));
console.log('anchored after cleaning:', anchored.test(field.trim().toLowerCase()));Example explained
Line 1The unanchored pattern only has to match somewhere, so a whole sentence containing an address reports true.
Line 2^ and $ force the pattern to describe the entire value, which is what a single form field means.
Line 3The raw field fails purely because of the surrounding spaces, not because the address is wrong.
Line 4trim().toLowerCase() produces the string you should both test and store, so later comparisons line up.
Important notes
new URL parses more schemes than you want to render: javascript: and data: URLs succeed, so compare u.protocol against an allowlist before putting the value into an href.
A domain is case-insensitive but the local part officially is not, so lowercase a copy for lookups and keep the typed string if you need to be exact; and avoid hardcoded TLD lists, since .technology and xn-- domains are real.
Common mistakes
Pasting a 200-character RFC email regex found online: nobody on the team can review or adjust it, it refuses deliverable addresses such as internationalized domains, and it still cannot spot the typo in ada@gmial.com, so real signups fail while bad data gets in anyway.
Leaving ^ and $ off a field validator, so 'call me: a@b.co, thanks' passes test() and the entire sentence is stored in the email column.
Checking a quantity with /\d+/ or with Number() alone: /\d+/ accepts '12px', and Number('') is 0, so a blank field silently becomes an order for zero.
Try it yourself
Change, predict, then run
In a browser console, paste a sentence containing two https links, one http link, and a bare example.com, then write a scanner that pulls out the candidates, strips trailing punctuation, skips anything new URL rejects, and logs hostname plus searchParams only for https. Then add an anchored numeric pattern that accepts -3.5 and .5 but rejects '', ' 7 ', 12px, and 0x1f.
Open the JavaScript workspaceCheck your understanding
A signup form validates the email field with /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/ and support still reports bounced confirmation mails. What is the most accurate conclusion?
- The pattern is too permissive, and swapping in a long RFC-based regex would stop the bounces.
- The anchors are the problem; without ^ and $ the pattern would inspect the whole value more carefully.
- No pattern can tell whether a mailbox exists, so some bounces are expected and delivery must be confirmed by sending mail.
- The {2,} should be replaced by a list of valid top-level domains so only real domains pass.
Show answer
Shape and existence are separate questions: ada@gmial.com satisfies any email regex, so no amount of pattern work removes bounces, and only a delivered message proves a mailbox. The first option is tempting because a stricter regex feels safer, but it mainly adds rejections of deliverable addresses while the typo still passes; removing the anchors would be worse still, since an entire sentence containing an address would validate.