JavaScript Interview Gotchas: The Footguns Most Engineers Miss

May 29, 202611 min read
dsaalgorithmsinterview-prepleetcode
JavaScript Interview Gotchas: The Footguns Most Engineers Miss
TL;DR
  • === vs ==: Abstract equality runs type coercions before comparing; the only intentional use of == is x == null to catch both null and undefined.
  • Number.isNaN vs global isNaN: The global coerces its argument first; Number.isNaN checks the actual value without coercion.
  • Array.sort() default: Without a comparator, sort converts elements to strings and sorts lexicographically, silently reordering numbers.
  • var closure bug: var is function-scoped so all loop callbacks share one binding; replace with let for per-iteration scope.
  • ?? vs ||: Nullish coalescing falls back only on null or undefined, preserving valid falsy values like 0 and "".
  • JSON.stringify silent data loss: Deep cloning via JSON drops undefined, functions, and corrupts Date and NaN; use structuredClone() instead.
  • Array.shift() is O(n): Using shift() in a BFS queue makes the traversal quadratic; use a head-pointer index to keep dequeue O(1).

JavaScript is forgiving. It takes almost anything you throw at it and tries to make it work. That permissiveness is what makes it approachable, and it is also what makes it a minefield.

Some gotchas you already know. typeof null === "object". NaN !== NaN. But the dangerous ones are subtler: code that looks correct, produces a value, and silently returns the wrong answer with no error in sight. In a live coding round, silent wrong answers are the worst kind. The sort that get you a "no hire" and a feedback form that says "seemed confident."

This guide covers the JavaScript behaviors most likely to cost you in an interview. The JavaScript collections cheat sheet covers data structures and what's missing from the standard library. This covers the language semantics itself.

== Does Not Compare Values. It Converts Them First.

The abstract equality operator runs a decision tree of type coercions before it checks anything. The results feel arbitrary until you read the spec, and most interviewers expect you to identify them as bugs on sight.

0 == false // true, false coerces to 0 "" == false // true, both coerce to 0 "" == 0 // true null == undefined // true null == 0 // false, null only equals undefined under == [] == false // true, [] → "" → 0, false → 0 [] == ![] // true, ![] is false, then [] == false

JavaScript did not invent chaos. It just formalized it into a spec. The rule: always use ===. The one intentional use case for == is x == null, which catches both null and undefined in a single check. If you write it, name the intent in a comment so the next person does not have a small crisis.

NaN Is a Number, and It Is Not Equal to Itself

NaN represents an invalid numeric computation. Its type is "number". It is the only value in JavaScript not equal to itself, by IEEE 754 specification.

typeof NaN // "number" NaN === NaN // false NaN !== NaN // true isNaN("") // false, "" coerces to 0, then 0 is-a-number isNaN([]) // false, [] coerces to 0 isNaN("abc") // true, but for the wrong reasons Number.isNaN(NaN) // true, no coercion, checks the actual value Number.isNaN("NaN") // false

The global isNaN coerces its argument before checking. Number.isNaN does not. In an interview, calling out the difference between the two is the kind of thing that makes an interviewer nod and write something down. Use Number.isNaN any time you are guarding against invalid computation results.

typeof Has Two Exceptions That Come Up Constantly

typeof is reliable for most primitives, but it has two holes that matter in interviews.

typeof null // "object", a 1995 design bug, never fixed for backward compat typeof [] // "object", not "array" typeof NaN // "number" typeof undefined // "undefined" typeof function(){} // "function"

For arrays, use Array.isArray(x). For null, guard explicitly: x !== null && typeof x === "object". Forgetting the null guard in tree traversal code produces Cannot read properties of null mid-interview. That is survivable, but it costs time and composure, both of which are scarce.

Array.sort() Stringifies Everything First

This is one of the most common JavaScript bugs in live interviews. Array.prototype.sort converts elements to strings and sorts lexicographically unless you pass a comparator. You will write [10, 9, 2, 1, 100].sort() and get [1, 10, 100, 2, 9] back, and it will take you a full minute to figure out why.

[10, 9, 2, 1, 100].sort() // [1, 10, 100, 2, 9], "10" < "2" lexicographically [10, 9, 2, 1, 100].sort((a, b) => a - b) // [1, 2, 9, 10, 100]

The subtraction comparator (a, b) => a - b works for normal ranges. For values near Number.MAX_SAFE_INTEGER, use (a, b) => a > b ? 1 : a < b ? -1 : 0 to avoid overflow. As of ECMAScript 2019, sort is required to be stable (V8 uses Timsort), so equal-key elements keep their original relative order.

var, let, and the Loop That Prints the Wrong Number

var is function-scoped. let and const are block-scoped. This produces the most-asked JavaScript closure question, and the answer is almost always "use let".

for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); } // Prints: 3 3 3 // One shared variable. By the time the callbacks run, the loop is done. for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); } // Prints: 0 1 2 // Each iteration creates a new binding.

The second trap is hoisting. var declarations are hoisted with initial value undefined, so accessing one before its assignment silently returns undefined. let and const throw a ReferenceError before their declaration. That window is the temporal dead zone, which sounds like a band name but is actually a source of bugs.

Floating Point: 0.1 + 0.2 Is Not 0.3

JavaScript has no integer type. Every number is a 64-bit IEEE 754 double-precision float. This matters in two places, and both will make you feel like you are going insane.

Precision:

0.1 + 0.2 === 0.3 // false 0.1 + 0.2 // 0.30000000000000004

For comparison: Math.abs(a - b) < Number.EPSILON.

Safe integer range:

Number.MAX_SAFE_INTEGER // 9007199254740991 (2^53 - 1) Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2 // true

Numbers above 2^53 - 1 lose precision silently. For interview problems involving large IDs or 64-bit values, the expected type is BigInt or string. Spotting the constraint and raising it before you code is a communication win. Your interviewer will notice.

JSON.stringify Silently Eats Your Data

The pattern JSON.parse(JSON.stringify(obj)) is popular for deep cloning. It corrupts data in four ways, none of which throw an error. It just quietly deletes your data and hands you back something that looks plausible.

const obj = { a: 1, b: undefined, // dropped entirely from the output c: () => {}, // dropped entirely d: new Date(), // becomes a string; JSON.parse gives you a string, not a Date e: NaN, // becomes null f: Infinity, // becomes null }; JSON.stringify(obj) // '{"a":1,"d":"2026-05-29T05:10:15.000Z","e":null,"f":null}' const arr = [1, undefined, 3]; JSON.stringify(arr) // "[1,null,3]", undefined becomes null in arrays const x = {}; x.self = x; JSON.stringify(x); // TypeError: Converting circular structure to JSON

For a real deep clone, use structuredClone(). It handles circular references, dates, maps, and sets correctly. It is available in Node 17+ and all modern browsers. If you reach for the JSON trick in an interview, acknowledge its limitations or swap to structuredClone. The interviewer knows about the footgun, and they are watching to see if you do too.

Always Pass a Radix to parseInt

parseInt accepts an optional second argument: the radix. Without it, the function infers the base from the string prefix. This was historically unpredictable, and the fact that it works fine in modern engines does not mean you should leave it implicit.

parseInt("0x10") // 16, hex prefix auto-detected parseInt("010") // 10 in modern engines, 8 (octal) in ES3 engines parseInt("08") // 8 now, but was 0 in some older runtimes // Always pass the radix: parseInt("10", 10) // 10 parseInt("10", 2) // 2, binary parseInt("ff", 16) // 255 // Additional traps: parseInt("1.9") // 1, stops at the decimal point parseInt("3 blind") // 3, stops at the space parseInt("abc") // NaN

Number("1.9") gives 1.9, Number("") gives 0, and Number("abc") gives NaN. The unary + operator behaves identically to Number(). Know which conversion you want before you write it. Picking the wrong one mid-interview is a recoverable mistake; not knowing the difference is not.

for...in Walks the Prototype Chain

for...in iterates all enumerable properties, including inherited ones. Any library that extends a prototype poisons every for...in loop in that runtime. It is a great footgun for codebases from 2012 and a great gotcha for interviews in 2026.

Object.prototype.debug = function() {}; // a library adds this const obj = { a: 1, b: 2 }; for (const key in obj) { console.log(key); // "a", "b", "debug", prototype property included } // Correct: for (const key of Object.keys(obj)) { console.log(key); // "a", "b" }

Never use for...in on arrays. It yields indices as strings, may include prototype methods, and does not guarantee insertion order on all engines. Use for...of for iterables and a standard for loop when you need the index.

?? and || Are Not Interchangeable

|| returns the right-hand side for any falsy left-hand value: 0, "", false, null, undefined. ?? returns the right-hand side only when the left is null or undefined. This distinction seems minor until you are writing config parsing code and a valid port number of 0 gets silently replaced.

const port = options.port || 3000; // Bug: if options.port === 0, this returns 3000, wrong const port = options.port ?? 3000; // Correct: 0 is a valid port number, only null/undefined triggers the fallback

Use ?? any time 0, "", or false are valid inputs. This applies to counts, booleans, empty strings, and numeric config values. The same logic governs optional chaining: obj?.value ?? defaultValue is the safe idiom for nested access with a fallback.

Object Equality Is Reference Equality

=== on objects and arrays checks identity, not structure.

{} === {} // false [] === [] // false const a = [1, 2, 3]; const b = a; a === b // true, same reference

You cannot use a JavaScript Set to deduplicate objects by value. A Set of objects deduplicates by reference, so two objects with identical fields are treated as different. For interview problems that require structural equality, serialize to a canonical string (careful of the JSON.stringify caveats above), write a deep-equals function, or restructure the solution to avoid the comparison entirely.

Array.shift() Is O(n)

Array.shift() removes the first element and re-indexes every remaining element. Using shift() inside a BFS loop makes the entire traversal O(n²), which fails on large inputs and will almost certainly get called out in a live interview.

const queue = [root]; while (queue.length) { const node = queue.shift(); // O(n) each call, quadratic total if (node.left) queue.push(node.left); if (node.right) queue.push(node.right); }

The fix for interviews is a head-pointer index: let head = 0; queue[head++] instead of shift(). That trades a small amount of memory for O(1) dequeue. See Array vs Linked List Performance for why this matters more than it looks on a whiteboard.

Quick Reference: JavaScript Interview Gotchas

FootgunSafe Alternative
x == yx === y
isNaN(x)Number.isNaN(x)
typeof x === "object" alonex !== null && typeof x === "object"
typeof x === "array"Array.isArray(x)
arr.sort()arr.sort((a, b) => a - b)
var in closureslet
JSON.parse(JSON.stringify(x))structuredClone(x)
parseInt(str)parseInt(str, 10)
for (key in obj)for (const key of Object.keys(obj))
a || default when 0/"" valida ?? default
queue.shift() in BFShead-pointer index or deque

These come up not just as direct questions but as silent bugs buried in your solution. The interviewer is often not looking for the bug; they are waiting to see if you catch it yourself. If you want to practice spotting exactly this kind of language-level gotcha under real interview conditions, SpaceComplexity runs voice-based mock interviews where the AI follows up on JavaScript-specific behavior in real time.

For more on language choice and what each interview format rewards, see Best Language for Coding Interviews.

Further Reading