Developer Guides
How to Validate an Email Address in JavaScript (Browser and Node.js)
To validate an email address in JavaScript, start with the browser’s built-in <input type="email"> check or a tested regex for instant feedback, then repeat the check on the server and add a DNS lookup to confirm the domain can receive mail. None of these prove the mailbox exists. That takes a confirmation email or a verification service. This guide walks through each layer with code that runs as written in current browsers and Node 22.
Layer 1: Let the browser do the first check
The cheapest email validation in JavaScript is the one you don’t write. An <input type="email"> already validates its value against the syntax defined in the WHATWG HTML standard, and you can read the result from the Constraint Validation API.
<form id="signup" novalidate>
<label for="email">Email</label>
<input id="email" name="email" type="email" required autocomplete="email" maxlength="254">
<p id="email-error" role="alert" hidden></p>
<button>Sign up</button>
</form>
<script>
const form = document.getElementById('signup');
const input = document.getElementById('email');
const error = document.getElementById('email-error');
form.addEventListener('submit', (event) => {
if (!input.checkValidity()) {
event.preventDefault();
error.textContent = input.validity.valueMissing
? 'Please enter your email address.'
: 'That doesn\'t look like a valid email address.';
error.hidden = false;
input.focus();
}
});
input.addEventListener('input', () => { error.hidden = true; });
</script>
A few details worth knowing, all documented on MDN’s input type=“email” page:
- An empty field counts as valid unless you add
required. - The browser strips leading and trailing whitespace from the value before validating.
- With the
multipleattribute, the field accepts a comma-separated list and validates each address. - You can add a
patternattribute for extra rules (for example, restricting to your company domain). The value must match both the pattern and the built-in email syntax.
validity.typeMismatch is the specific flag that’s set when a non-empty value isn’t a valid email; checkValidity() covers it along with required and pattern.
The browser’s rule is intentionally loose. It accepts user@localhost (no dot in the domain) and doesn’t reject consecutive dots in the local part. MDN is also blunt that HTML form validation is not a security measure: anyone can edit the page or send a request directly, so the server must check again.
Layer 2: A regex you can reuse anywhere
If you need the same rule in client code, server code, and maybe a CSV import script, a regular expression is the portable option. This is the pattern from our email validation regex guide, which explains every part of it:
export const EMAIL_RE =
/^(?=.{1,254}$)(?=[^@]{1,64}@)(?!\.)(?!.*\.\.)[A-Za-z0-9.!#$%&'*+\/=?^_`{|}~-]+(?<!\.)@(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/;
export function isEmailSyntax(value) {
return EMAIL_RE.test(String(value).trim());
}
isEmailSyntax('jane.doe+news@example.com'); // true
isEmailSyntax('jane..doe@example.com'); // false
isEmailSyntax('jane@localhost'); // false
Compared with the browser rule, it adds the 64-character local-part limit and 254-character total limit from RFC 5321 (the 254 figure comes from erratum 1690 to RFC 3696), requires a dot in the domain, and rejects leading, trailing, or doubled dots before the @.
What it deliberately doesn’t accept: quoted local parts like "john doe"@example.com, IP address literals like user@[192.0.2.1], and non-ASCII characters. Those are legal in the standards but rare in practice. If you need Unicode support, read internationalized email addresses first.
A word on the popular one-liner /^\S+@\S+\.\S+$/: it’s fine as a “did they type something email-like” check, but it accepts a@b.c@d.e and "@x.y, so don’t use it as your only server-side rule.
Layer 3: Use validator.js when you’d rather not own a regex
validator.js is a widely used string validation library that works in the browser and Node. Install it:
npm i validator
import validator from 'validator';
validator.isEmail('user@example.com'); // true
validator.isEmail('user@localhost'); // false (require_tld defaults to true)
validator.isEmail('Jane <jane@example.com>'); // false
validator.isEmail('Jane <jane@example.com>', { allow_display_name: true }); // true
validator.isEmail('ünïcode@example.com'); // true
validator.isEmail('ünïcode@example.com', { allow_utf8_local_part: false }); // false
According to the project README, isEmail defaults to allow_display_name: false, allow_utf8_local_part: true, require_tld: true, allow_ip_domain: false, and allow_underscores: false, among other options. Two of those defaults differ from the regex above: validator.js accepts quoted local parts and UTF-8 local parts out of the box. Decide whether you want that. Accepting a Unicode local part only helps if your sending infrastructure supports SMTPUTF8.
Layer 4: Check the domain in Node.js
Syntax checks can’t catch jane@gmail.con or a domain that was never set up for email. On the server you can ask DNS. The rules come from RFC 5321 section 5.1: look up MX records; if there are none, the domain’s own A or AAAA record acts as an implicit MX. And RFC 7505 adds the “null MX”, a single MX record with an empty exchange that means the domain accepts no mail at all.
Here’s a dependency-free module for Node 22 using node:dns/promises:
// validate-email.mjs (Node 22+, no dependencies)
import { Resolver } from 'node:dns/promises';
const EMAIL_RE =
/^(?=.{1,254}$)(?=[^@]{1,64}@)(?!\.)(?!.*\.\.)[A-Za-z0-9.!#$%&'*+\/=?^_`{|}~-]+(?<!\.)@(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/;
const resolver = new Resolver({ timeout: 3000, tries: 2 });
async function hasAddressRecord(domain) {
for (const lookup of [resolver.resolve4.bind(resolver), resolver.resolve6.bind(resolver)]) {
try {
if ((await lookup(domain)).length > 0) return true;
} catch (err) {
if (err.code !== 'ENODATA' && err.code !== 'ENOTFOUND') throw err;
}
}
return false;
}
export async function validateEmail(input) {
const email = String(input).trim();
if (!EMAIL_RE.test(email)) return { ok: false, reason: 'bad_syntax' };
const domain = email.slice(email.lastIndexOf('@') + 1).toLowerCase();
let mx;
try {
mx = await resolver.resolveMx(domain);
} catch (err) {
if (err.code === 'ENOTFOUND') return { ok: false, reason: 'no_such_domain' };
if (err.code === 'ENODATA') mx = [];
else return { ok: null, reason: 'dns_error', code: err.code }; // timeout, SERVFAIL: unknown, not invalid
}
// RFC 7505 null MX: a single record with an empty exchange means "no mail here".
if (mx.length === 1 && mx[0].exchange === '') return { ok: false, reason: 'null_mx' };
if (mx.length > 0) {
const hosts = mx.sort((a, b) => a.priority - b.priority).map((r) => r.exchange);
return { ok: true, reason: 'mx_found', mx: hosts };
}
// No MX records: RFC 5321 section 5.1 falls back to the domain's own A/AAAA (implicit MX).
try {
return (await hasAddressRecord(domain))
? { ok: true, reason: 'implicit_mx', mx: [domain] }
: { ok: false, reason: 'no_mail_server' };
} catch (err) {
return { ok: null, reason: 'dns_error', code: err.code };
}
}
// Example run: node validate-email.mjs someone@gmail.com
if (process.argv[2]) console.log(await validateEmail(process.argv[2]));
Run it:
node validate-email.mjs someone@gmail.com
node validate-email.mjs someone@example.com # { ok: false, reason: 'null_mx' }
example.com is a handy test: it publishes a null MX, so Node returns [{ exchange: '', priority: 0 }]. A nonexistent domain rejects with ENOTFOUND, and a domain that exists but has no MX records rejects with ENODATA. Treat timeouts and server failures as “unknown”, not “invalid”, or a flaky resolver will reject real customers. We go deeper on resolvers, caching, and error codes in how to do an MX lookup in Node.js.
What JavaScript validation still can’t tell you
After syntax and DNS, you know the address is well formed and the domain is set up for mail. You still don’t know:
- Whether the mailbox exists. Checking that requires an SMTP conversation with the recipient’s server, which many providers answer unreliably, and outbound port 25 is blocked by default on major cloud platforms. See how SMTP email verification works for the details.
- Whether it’s a typo of a real domain.
gmial.commay have DNS records. You need a list of common domains and their misspellings. - Whether it’s disposable. Throwaway inbox services have valid MX records by design. Blocking them takes a maintained domain list.
- Whether the person owns it. Only a confirmation link does that.
Calling a verification API from JavaScript
If you’d rather not maintain disposable lists, typo tables, and DNS edge cases, the API on this site runs those checks in one request. Call it from your server, not the browser, so your key stays private:
// Node 22+: fetch is built in
const BASE = 'https://bookmyspot.us/tools/email-verifier/api/v1';
const res = await fetch(`${BASE}/verify?email=${encodeURIComponent('jane@gmial.com')}`, {
headers: { 'X-API-Key': process.env.VERIFIER_API_KEY },
});
if (!res.ok) throw new Error(`Verifier returned ${res.status}`);
const result = await res.json();
// result.status is "valid", "risky", or "invalid"
// result.suggestion is a typo fix such as "jane@gmail.com", or null
console.log(result.status, result.reason, result.suggestion);
The response includes status, reason, a checks object (syntax, mx, disposable, role, free_provider), the MX hosts, and a typo suggestion. It doesn’t connect to mail servers to test mailboxes. Free keys with a daily limit are on the API documentation page, and the email verification API guide shows timeouts, bulk calls, and how to act on each status. To check a single address by hand, use the free email verifier.
For the full signup flow, including when to show the typo suggestion and when to block, see real-time email verification for signup forms.
FAQ
What’s the simplest way to validate an email in JavaScript?
Use <input type="email" required> and call checkValidity() before submitting. It uses the browser’s built-in syntax rule and needs no regex. Repeat the check on the server, because client-side validation can be bypassed.
Is validator.js better than a regex?
It saves you from maintaining a pattern and offers options like allow_display_name and require_tld. Its defaults accept quoted and UTF-8 local parts, which a simple regex usually rejects. Pick whichever matches the rule you want and use the same one everywhere.
Can JavaScript check if an email address actually exists?
Not from the browser. On the server, Node can confirm the domain has mail servers with an MX lookup, but confirming the mailbox needs an SMTP exchange that many providers don’t answer honestly. A confirmation email is the reliable proof.
Should I run the DNS check on every keystroke?
No. Run syntax checks as the user types and the DNS or API check on blur or submit, with a short timeout. Cache results per domain so repeated lookups for gmail.com don’t hit DNS every time.
Free email verifier: syntax, MX, disposable and role-based addresses, typos. Paste up to 100 at once.