HelpWithWebGet Help Now
← Back to Blog
JavaScript9 min read

JavaScript Gotchas: Equality, Hoisting, and `this` Surprises

Why typeof null is object, why == coerces in weird ways, why var-in-a-loop captures the wrong value, and a dozen other JavaScript quirks that bite even senior devs.

ByDino Bartolome
Binary code patterns on a blue screen
Photo by Ilya Pavlov on Unsplash

JavaScript has 25+ years of accumulated weirdness baked into the language. Most of the time you don't notice — modern code uses ===, let, and arrow functions and sidesteps the worst of it. But the moment you read someone else's code, debug a legacy app, or take a job interview, the gotchas resurface.

Here are the ones that catch even experienced devs.

1. typeof null returns "object"

The most famous JavaScript bug. It's been in the language since 1995 and won't ever be fixed (changing it would break the internet).

``javascript typeof null // "object" typeof undefined // "undefined" typeof NaN // "number" typeof [] // "object" typeof function(){} // "function" ``

If you want to check for null, use value === null. If you want to check for "is this object-like," use both: value !== null && typeof value === "object".

2. NaN is not equal to itself

``javascript NaN === NaN // false NaN == NaN // false ``

To check for NaN, use Number.isNaN(value) (not the older isNaN(), which coerces strings — isNaN("hello") returns true).

3. Loose equality coerces in non-obvious ways

== runs a chain of coercions that produces surprising results:

``javascript [] == false // true ([] → "" → 0 → false) [] == 0 // true (same chain) "" == 0 // true ("" → 0) "0" == false // true (both → 0) "0" == 0 // true ("0" → 0) null == undefined // true (special-case) null == 0 // false (null only loose-equals undefined) [] == ![] // true (![] is false, [] coerces to 0, false coerces to 0) ``

The fix is the same one you've heard a thousand times: use ===. Always.

4. Falsy values: memorize the six

These six are the only falsy values in JavaScript:

`` false, 0, "", null, undefined, NaN ``

Everything else is truthy, including:

``javascript Boolean("0") // true (non-empty string) Boolean("false") // true (non-empty string) Boolean([]) // true (object) Boolean({}) // true (object) Boolean(new Boolean(false)) // true (object wrapper!) ``

That last one bites people who do if (obj) checks on Boolean wrappers.

5. var hoists, let and const don't (kind of)

Technically all three hoist, but let and const are in the "temporal dead zone" until their declaration line. Accessing them before throws.

```javascript console.log(a); // undefined (var is hoisted with undefined) var a = 1;

console.log(b); // ReferenceError (let is in TDZ) let b = 1; ```

This matters in interview questions and in legacy code that mixes var with let/const.

6. var is function-scoped, not block-scoped

```javascript for (var i = 0; i < 3; i++) { // ... } console.log(i); // 3 — i leaks out of the loop

for (let j = 0; j < 3; j++) { // ... } console.log(j); // ReferenceError — j is block-scoped ```

The classic interview gotcha that builds on this:

```javascript for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); } // Prints: 3, 3, 3 — all callbacks reference the same i

for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); } // Prints: 0, 1, 2 — each iteration gets its own i ```

7. this in arrow functions vs regular functions

Arrow functions don't have their own this — they inherit it from the enclosing lexical scope.

```javascript const obj = { name: "Alice", regular: function() { console.log(this.name); }, arrow: () => console.log(this.name), };

obj.regular(); // "Alice" (this = obj) obj.arrow(); // undefined (this = enclosing scope, probably window/global) ```

This is why arrow functions are NOT a drop-in replacement for object methods. They're great for callbacks where you want to preserve the outer this (React class components, event handlers), but they break when used as methods.

8. Array.sort() sorts as strings by default

```javascript [10, 2, 1, 20].sort(); // [1, 10, 2, 20] — sorted as strings!

[10, 2, 1, 20].sort((a, b) => a - b); // [1, 2, 10, 20] — proper numeric sort ```

Always pass a compare function for numbers.

9. delete on arrays leaves holes

``javascript const arr = [1, 2, 3]; delete arr[1]; console.log(arr); // [1, empty, 3] console.log(arr.length); // 3 — length unchanged console.log(arr[1]); // undefined ``

Use splice() to actually remove: arr.splice(1, 1)[1, 3].

10. JSON.stringify silently drops things

```javascript JSON.stringify({ a: 1, b: undefined, c: function(){}, d: Symbol() }); // '{"a":1}' — undefined, functions, and symbols are dropped

JSON.stringify({ a: NaN, b: Infinity }); // '{"a":null,"b":null}' — non-finite numbers become null

JSON.stringify({ d: new Date() }); // '{"d":"2026-06-14T00:00:00.000Z"}' — Date is converted to ISO string

const circular = {}; circular.self = circular; JSON.stringify(circular); // TypeError: Converting circular structure to JSON ```

If you need to preserve undefined/functions/circular refs, use a library like flatted or devalue.

11. Object property keys are coerced to strings

``javascript const obj = {}; obj[1] = "a"; obj["1"] = "b"; obj[true] = "c"; obj["true"] = "d"; console.log(obj); // { '1': 'b', true: 'd' } ``

If you need non-string keys, use Map instead of plain objects.

Need help debugging legacy JavaScript?

Most JS bugs come down to one of these gotchas plus a deadline. If you're stuck on a quirky legacy codebase or a production issue you can't reproduce locally, we can help — senior-dev eyes on your code without the agency markup.

Need Help With Your Website?

I fix these problems every day. Send me a message and I'll take a look.

Get Help Now