A collection of fun and tricky JavaScript bugs that test your understanding of how JavaScript really works.
Great for workshops, interviews, or self-study!
const buttons = [];
for (var i = 0; i < 3; i++) {
buttons.push(function () {
console.log(`Button ${i} clicked`);
});
}
buttons[0](); // ??
buttons[1](); // ??
buttons[2](); // ??π‘ Answer
They all log Button 3 clicked.
Because var is function-scoped, not block-scoped, all closures share the same i β which ends at 3 after the loop.
const obj = {
value: 42,
getValue: () => {
return this.value;
},
};
console.log(obj.getValue()); // ??π‘ Answer
Logs undefined.
Arrow functions don't bind their own this β they use the one from their outer lexical scope (likely the global object in this case).
const a = {};
const b = { key: "b" };
const c = { key: "c" };
a[b] = 123;
a[c] = 456;
console.log(a[b]); // ??π‘ Answer
Logs 456.
Objects used as keys are converted to the same string: "[object Object]". So b and c overwrite the same key.
const promise = new Promise((resolve, reject) => {
console.log("Promise created");
resolve("Done");
});
promise.then((res) => console.log(res));
console.log("After promise");π‘ Answer
Logs:
Promise created
After promise
Done
Creating a Promise runs its executor function immediately (sync). .then() runs async (microtask).
console.log("Start");
setTimeout(() => {
console.log("Timeout");
}, 0);
Promise.resolve().then(() => {
console.log("Promise");
});
console.log("End");π‘ Answer
Logs:
Start
End
Promise
Timeout
Microtasks (Promise.then) run before macrotasks (setTimeout), even with 0 delay.
console.log([] == false); // true
console.log([] == ![]); // trueπ‘ Answer
[] == falseβtruedue to coercion:[]β''βfalse.[] == ![]β[] == falseβ againtrue.
function Wizard() {}
Wizard.prototype.castSpell = function () {
return "π₯ Fireball!";
};
const merlin = new Wizard();
merlin.castSpell = () => "β¨ Sparkles!";
delete merlin.castSpell;
console.log(merlin.castSpell()); // ??π‘ Answer
Logs 'π₯ Fireball!'.
Deleting the instance method reveals the prototype method.
const arr = [1, 2, 3];
arr[10] = 11;
console.log(arr.length); // ??
console.log(arr.map((x) => x * 2)); // ??π‘ Answer
arr.lengthis11(last index + 1).mapskips empty slots, so[2, 4, 6, <7 empty items>, 22].
const weird = [0, null, undefined, false, "", NaN];
for (let value of weird) {
if (value) {
console.log(`${value} is truthy`);
} else {
console.log(`${value} is falsy`);
}
}π‘ Answer
All log as falsy. These are the six falsy primitives in JS.
function test() {
console.log(value);
let value = 10;
}
test();π‘ Answer
Throws a ReferenceError. let is hoisted but not initialized β it's in the Temporal Dead Zone.
console.log(typeof null); // ??π‘ Answer
Returns 'object'.
This is a long-standing bug in JavaScript β null is not actually an object, but typeof null returns 'object'.
console.log(NaN === NaN); // ??π‘ Answer
Returns false.
NaN is the only value in JS that is not equal to itself.
console.log(3 > 2 > 1); // ??π‘ Answer
Returns false.
3 > 2 β true β true > 1 β 1 > 1 β false.
hoisted();
function hoisted() {
console.log("I am hoisted");
}
notHoisted();
var notHoisted = function () {
console.log("I am not hoisted");
};π‘ Answer
hoisted()runs fine.notHoisted()throwsTypeError: notHoisted is not a function.
Only function declarations are hoisted with their definitions. var is hoisted as undefined.
function surprise() {
oops = 42;
}
surprise();
console.log(oops); // ??π‘ Answer
Logs 42.
Undeclared variables become implicit globals (in sloppy mode).
const { length } = undefined;π‘ Answer
Throws TypeError: Cannot destructure property.
You can't destructure properties from undefined or null.
const arr = [1, , 3];
console.log(arr[1]); // ??
console.log(1 in arr); // ??π‘ Answer
arr[1]logsundefined.1 in arrreturnsfalse.
There's a hole in the array, not an actual undefined value.
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}π‘ Answer
Logs: 3, 3, 3.
All callbacks share the same i (3) due to var. Use let to capture each iteration.
function foo() {
return;
{
ok: true;
}
}
console.log(foo()); // ??π‘ Answer
Returns undefined.
The return statement is terminated before the object β it's a line break bug.
console.log(parseInt("08")); // ??π‘ Answer
Returns 8, but older engines may interpret '08' as octal and return 0.
Always pass radix: parseInt('08', 10).
console.log(isNaN("foo")); // ??
console.log(Number.isNaN("foo")); // ??π‘ Answer
isNaN('foo')βtrue(due to coercion).Number.isNaN('foo')βfalse.
Use Number.isNaN to avoid coercion.
function test(x) {
arguments[0] = 99;
return x;
}
console.log(test(42)); // ??π‘ Answer
Returns 99.
In non-strict mode, arguments and named params are linked.
console.log(!!"false" == !!"true"); // ??π‘ Answer
Returns true.
Both 'false' and 'true' are non-empty strings β truthy.
console.log([1, 2] + [3, 4]); // ??π‘ Answer
Returns '1,23,4'.
Arrays are coerced to strings: '1,2' + '3,4'.
console.log(1 + {}); // ??π‘ Answer
Returns '1[object Object]'.
Object gets coerced to string.
console.log(+{}); // ??π‘ Answer
Returns NaN.
+{} β Number('[object Object]') β NaN.
console.log("5" - "2"); // ??
console.log("5" + "2"); // ??π‘ Answer
'5' - '2'β3(coerced to numbers).'5' + '2'β'52'(string concatenation).
console.log(typeof Function.prototype); // ??π‘ Answer
Returns 'function'.
Function prototype is still a function.
console.log([] instanceof Array); // ??
console.log([] instanceof Object); // ??
console.log(function () {} instanceof Function); // ??π‘ Answer
They're all true.
Everything in JS is ultimately an object, and functions are also objects.
var x = 10;
console.log(delete x); // ??π‘ Answer
Returns false.
delete only works on object properties, not variables declared with var, let, or const.
What will the result of the following code be?
for (;;) {
console.log("What will happen?");
}π‘ Answer
Infinite Loop.
Technically the for loop will run although since there's no exit condition it will create an infinite loop π€―