`===` compares value and type with no conversion; `==` converts the two sides to a common type first, then compares. The conversion rules are complicated enough that `==` produces genuinely surprising results, which is why the standard advice is to always use `===`.
The examples that make it concrete:
- `5 === "5"` is `false` (number vs string). `5 == "5"` is `true` — the string gets converted to a number.
- `0 == false` is `true`. `0 === false` is `false`.
- `"" == false` is `true`. `null == undefined` is `true`, but `null === undefined` is `false`.
- `"0" == false` is `true`, yet `"0" == ""` is `false` — this is the kind of inconsistency that makes `==` untrustworthy.
Why this causes real bugs: a form input always gives you a string. If a user types `0` and you check `if (value == false)`, that's true — and now an entered zero is treated as an empty field. With `===` the comparison fails loudly instead of silently doing the wrong thing, and you're forced to convert deliberately: `Number(value) === 0`.
The rule to follow: use `===` and `!==` everywhere by default. If you need types to differ, convert explicitly (`Number(x)`, `String(x)`, `Boolean(x)`) so the intent is visible in the code. Your future self reading it can see what you meant.
The one commonly accepted exception: `x == null` is a compact way to check for 'null or undefined' at once, and many codebases allow it deliberately. Some style guides still prefer `x === null || x === undefined` for explicitness. Either is defensible; just be consistent.
A related trap worth knowing while you're here: `===` on objects and arrays compares references, not contents. `[1,2] === [1,2]` is `false`, because they're two different arrays that happen to look alike. Comparing object contents needs a deliberate comparison of the fields (or a helper). This catches almost everyone once.
Turn on a linter — ESLint's `eqeqeq` rule flags every `==` automatically, so you don't have to rely on remembering.