In JavaScript, not equal comes in two flavors — != (loose) and !== (strict) — and reaching for the wrong one in a security check can silently open an authentication bypass. The difference is not cosmetic. The loose != operator performs type coercion before comparing, so 0 != "" is false and null != undefined is false, results that surprise almost everyone the first time they see them. When those coercion quirks land inside a comparison that gates access or validates input, an attacker who understands them can slip a value past your check that you never intended to allow.
This guide explains how the JavaScript not equal operators actually behave, shows the security failure modes, and gives you a rule you can apply without thinking.
The two operators, precisely
!== is strict inequality. It returns true unless both operands have the same type and the same value. No conversion happens.
!= is loose inequality (also called abstract inequality). Before comparing, it applies the Abstract Equality Comparison algorithm from the ECMAScript spec, which coerces operands toward a common type. Only then does it check for inequality.
That coercion step is where the trouble starts. Consider these, all of which evaluate to false — meaning the values are treated as equal under loose comparison:
0 == ""; // true
0 == "0"; // true
"" == false; // true
null == undefined; // true
[] == false; // true
"0" == false; // true
Flip each to != and they return false, meaning "not not-equal," meaning your if (a != b) branch does not fire. The strict versions behave the way you'd expect: 0 !== "" is true, null !== undefined is true.
Where JavaScript not equal becomes a security bug
The classic failure is a comparison that guards a privileged path. Suppose a route checks a role or a token against an expected value:
// Vulnerable: loose comparison
function isForbidden(role) {
return role != "admin"; // intended: block everyone who isn't admin
}
That specific example is fine for strings, but the danger appears the moment either side can be a non-string that coerces. A more realistic bug is a numeric check against user-supplied JSON:
// Attacker sends { "accessLevel": [] } as JSON
if (req.body.accessLevel != 0) {
denyAccess();
}
Here [] != 0 is false, because [] coerces to "" then to 0. The denyAccess() branch never runs, and an empty array just walked through a check meant for the number zero. The same trap catches "0", false, and null in various combinations. An attacker probing a JSON API does not need to guess your logic; they just feed values that coerce their way past it.
Another sharp edge is comparing to null. x != null is loose and, by spec, matches both null and undefined. Some developers rely on that as a shorthand for "has a value," which is occasionally intentional — but if you meant strictly not-null, you have accidentally let undefined through too. Being explicit removes the ambiguity.
The rule: default to strict
Use !== and === everywhere unless you have a specific, documented reason to want coercion. This is not a stylistic preference; it eliminates an entire category of comparison bug. The industry consensus is strong enough that linters enforce it. Turn on the ESLint eqeqeq rule:
{
"rules": {
"eqeqeq": ["error", "always"]
}
}
With eqeqeq set to error, any != or == in the codebase fails the build, and a reviewer never has to argue about it in a pull request. The rule allows a smart mode that permits == null as an intentional null-or-undefined check, but for security-sensitive code I'd keep it strict and write === null || === undefined explicitly so the intent is visible.
Coercion isn't the only comparison hazard
Even strict comparison has limits. NaN !== NaN is true — NaN is not equal to anything, including itself — so use Number.isNaN(x) to test for it rather than x !== NaN, which is always true and useless.
Object comparison is by reference, not value: {a:1} !== {a:1} is true because they are two different objects. If you need structural inequality, compare serialized forms or use a deep-equality helper. And when comparing secrets — a session token, an HMAC signature — neither !== nor != is appropriate, because both short-circuit on the first differing character and leak timing information. Use a constant-time comparison such as Node's crypto.timingSafeEqual:
const crypto = require("crypto");
const a = Buffer.from(userToken);
const b = Buffer.from(expectedToken);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
reject();
}
The length check first is deliberate: timingSafeEqual throws if the buffers differ in length, so you compare lengths with !== and only call the constant-time function when they match.
Validate types before you compare at all
The deepest fix is to not let ambiguous types reach a comparison. Coerce and validate input at the boundary. If accessLevel is supposed to be a number, parse and check it before any logic runs:
const level = Number(req.body.accessLevel);
if (!Number.isInteger(level)) {
return reject("invalid accessLevel");
}
if (level !== 0) {
denyAccess();
}
Now an array or a string never reaches the !==, because it was rejected as a non-integer up front. A schema validator like zod or ajv does this systematically across every field, which is far more reliable than remembering to guard each comparison by hand. Input validation is the layer that makes the not-equal question moot — see our Academy material on validating untrusted input for the broader pattern.
For the code you don't write, the same class of coercion bug hides in dependencies, and an SCA tool such as Safeguard can flag known-vulnerable packages where these logic flaws have already been reported and patched.
FAQ
What is the difference between != and !== in JavaScript?
!= is loose (abstract) inequality: it coerces both operands to a common type before comparing, so 0 != "" is false. !== is strict inequality: it returns true unless the operands share both type and value, with no coercion. For the JavaScript not equal check in almost every case, use !==.
Why is loose inequality a security risk?
Because type coercion produces results attackers can exploit. Values like [], "0", false, and null coerce in ways that let them equal 0 or an empty string under loose comparison, so a check written with != can be bypassed by feeding a coercing value into a JSON API.
How do I enforce strict comparison across a codebase?
Enable the ESLint eqeqeq rule set to error. It fails the build on any == or !=, so the strict operators become the only option and reviewers never debate it.
Is !== safe for comparing passwords or tokens?
No. Both !== and != short-circuit on the first differing character and leak timing information. Use a constant-time comparison like crypto.timingSafeEqual for secrets, after checking lengths match.