DEV Community

Cover image for 25 Challenging JavaScript Questions Every Developer Should Be Ready For
Gouranga Das Samrat
Gouranga Das Samrat

Posted on

25 Challenging JavaScript Questions Every Developer Should Be Ready For

25 Challenging JavaScript Questions Every Developer Should Be Ready For

JavaScript is full of quirks, hidden behaviors, and subtle pitfalls that can trip up even experienced developers. In this article, I’ve compiled 25 tricky JavaScript questions that test your understanding of hoisting, scoping, coercion, the event loop, closures, and more. Each question comes with the expected console output and a clear, brief explanation to help you master JavaScript’s nuances. Whether you’re prepping for interviews or just want to sharpen your skills, these brain teasers will deepen your knowledge and challenge your assumptions.

1. Hoisting with var

console.log(a);
var a = 10;
Enter fullscreen mode Exit fullscreen mode

Output: undefined

Explanation:
JavaScript hoists variable declarations (var) to the top of their scope. However, only the declaration is hoisted, not the assignment. So a is declared as undefined at the top, and the 10 is assigned later.

2. Hoisting with let

console.log(b);
let b = 10;
Enter fullscreen mode Exit fullscreen mode

Output: ReferenceError

Explanation:
Variables declared with let are hoisted too, but they are not initialized. They remain in a “temporal dead zone” from the start of the block until the declaration is evaluated, which makes accessing them before declaration illegal.

3. Function Declaration Hoisting

foo();
function foo() {
  console.log("Hello");
}
Enter fullscreen mode Exit fullscreen mode

Output: Hello

Explanation:
Function declarations are hoisted with their full definition. This means foo() can be called before its declaration because the whole function is moved to the top during compilation.

4. Function Expression Hoisting

bar();
var bar = function () {
  console.log("Hi");
};
Enter fullscreen mode Exit fullscreen mode

Output: TypeError: bar is not a function

Explanation:
Although bar is hoisted as a var, it is only assigned undefined during hoisting. The actual function expression isn’t hoisted, so trying to invoke bar() before the function is assigned results in an error.

5. Arrow Function vs Regular Function (this)

const obj = {
  name: "JS",
  regular: function () {
    return this.name;
  },
  arrow: () => {
    return this.name;
  },
};
console.log(obj.regular()); // 'JS'
console.log(obj.arrow()); // undefined
Enter fullscreen mode Exit fullscreen mode

Explanation:
Regular functions use dynamic this, so obj.regular() has this pointing to obj. Arrow functions inherit this from their surrounding scope (in this case, the global scope), which does not have a name property.

6. setTimeout with var in Loop

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 1000);
}
Enter fullscreen mode Exit fullscreen mode

Output: 3 3 3

Explanation:
var is function-scoped, so the same i is shared in each iteration. By the time setTimeout callbacks run, the loop has completed, and i is 3.

7. setTimeout with let in Loop

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 1000);
}
Enter fullscreen mode Exit fullscreen mode

Output: 0 1 2

Explanation:
let is block-scoped, so each iteration has its own copy of i. Each callback gets the correct i value for that iteration.

8. typeof null

console.log(typeof null);
Enter fullscreen mode Exit fullscreen mode

Output: 'object'

Explanation:
This is a known JavaScript quirk. null is a primitive, but typeof null incorrectly returns 'object' due to legacy reasons in JavaScript's initial implementation.

9. == vs ===

console.log(0 == "0"); // true
console.log(0 === "0"); // false
Enter fullscreen mode Exit fullscreen mode

Explanation:
== performs type coercion, so '0' is converted to a number. === checks both type and value without coercion, so number 0 is not equal to string '0'.

10. Objects as Keys

const a = {};
const b = {};
a[b] = "hello";
console.log(a[b]);
Enter fullscreen mode Exit fullscreen mode

Output: 'hello'

Explanation:
When using an object as a key, JavaScript converts it to a string — typically "[object Object]". So both a[b] and a["[object Object]"] refer to the same key.

11. Type Coercion with Arrays and Objects

console.log([] + []); // ""
console.log([] + {}); // "[object Object]"
console.log({} + []); // 0 (or "[object Object]" in some contexts)
Enter fullscreen mode Exit fullscreen mode

Explanation:
Adding arrays and objects triggers toString() or value coercion. [] + [] is "", [] + {} becomes "" + "[object Object]", and {} can be interpreted as a code block in some contexts, which can lead to confusing results.

12. Event Loop Execution

console.log("start");
setTimeout(() => console.log("timeout"), 0);
Promise.resolve().then(() => console.log("promise"));
console.log("end");
Enter fullscreen mode Exit fullscreen mode

Output:
start
end
promise
timeout

Explanation:
JavaScript executes synchronous code first, then microtasks (promises), and finally macrotasks (like setTimeout).

13. Variable Shadowing

let x = 5;
function test() {
  let x = 10;
  console.log(x);
}
test();
console.log(x);
Enter fullscreen mode Exit fullscreen mode

Output:
10
5

Explanation:
The variable x inside test shadows the outer x. Each x exists in its own scope, so changes in one don't affect the other.

14. Object Reference Behavior

let obj1 = { name: "A" };
let obj2 = obj1;
obj2.name = "B";
console.log(obj1.name);
Enter fullscreen mode Exit fullscreen mode

Output: 'B'

Explanation:
Both obj1 and obj2 reference the same object in memory. Changing a property through one reference reflects on the other.

15. Closure and Persistent Variables

function outer() {
  let count = 0;
  return function () {
    count++;
    console.log(count);
  };
}
const counter = outer();
counter(); // 1
counter(); // 2
Enter fullscreen mode Exit fullscreen mode

Explanation:
The returned function forms a closure, retaining access to the count variable even after outer finishes executing.

16. Implicit Global Variables

(function () {
  var x = (y = 5);
})();
console.log(typeof x); // undefined
console.log(typeof y); // number
Enter fullscreen mode Exit fullscreen mode

Explanation:
Here, y = 5 creates a global variable since it's not declared with var, let, or const. x is block-scoped due to var.

17. arguments and Parameter Link

function foo(a, b) {
  arguments[0] = 99;
  console.log(a);
}
foo(1, 2);
Enter fullscreen mode Exit fullscreen mode

Output: 99

Explanation:
In non-strict mode, function arguments are linked with the arguments object. Changing arguments[0] changes a.

18. Default Destructuring Values

const [a = 1, b = 2] = [undefined, null];
console.log(a, b);
Enter fullscreen mode Exit fullscreen mode

Output: 1 null

Explanation:
a gets the default because the value is undefined. b does not use the default because null is a valid value.

19. Array Holes and Length

const arr = [1, , 3];
console.log(arr.length); // 3
console.log(arr[1]); // undefined
Enter fullscreen mode Exit fullscreen mode

Explanation:
A missing element in an array creates a “hole” — an index with no value set, though the length still counts it. Accessing it returns undefined.

20. this inside setTimeout

const person = {
  name: "Alice",
  greet: function () {
    setTimeout(function () {
      console.log(this.name);
    }, 1000);
  },
};
person.greet();
Enter fullscreen mode Exit fullscreen mode

Output: undefined

Explanation:
Inside setTimeout, this refers to the global object, not person, because it's a regular function. this.name is therefore undefined.

21. Immutable Object with Object.freeze

const obj = Object.freeze({ name: "Test" });
obj.name = "Changed";
console.log(obj.name);
Enter fullscreen mode Exit fullscreen mode

Output: 'Test'

Explanation:
Object.freeze prevents modification of object properties. Attempts to change them fail silently in non-strict mode.

22. Object Destructuring Order

const { a, b } = { b: 1, a: 2 };
console.log(a, b);
Enter fullscreen mode Exit fullscreen mode

Output: 2 1

Explanation:
Destructuring is based on property names, not order. So a gets the value of a and b gets b.

23. Spread Creates a Copy

const arr = [1, 2, 3];
const copy = [...arr];
copy[0] = 9;
console.log(arr[0]);
Enter fullscreen mode Exit fullscreen mode

Output: 1

Explanation:
Using the spread operator ... creates a shallow copy. Changing the copy doesn’t affect the original.

24. NaN Comparison

console.log(NaN === NaN);
Enter fullscreen mode Exit fullscreen mode

Output: false

Explanation:
NaN is not equal to anything, including itself. This is a unique behavior in JavaScript (and some other languages).

25. typeof a Function

console.log(typeof function () {});
Enter fullscreen mode Exit fullscreen mode

Output: 'function'

Explanation:
Functions are a special kind of object in JavaScript, and typeof can uniquely identify them as 'function'.

Conclusion

JavaScript can often surprise even seasoned developers with its quirks and hidden behaviors. The key to mastering these tricky scenarios isn’t just knowing the right answer, but truly understanding the reasoning behind it. When you carefully read and digest the explanations, patterns begin to emerge — and what once seemed confusing starts to make sense. Whether it’s hoisting, closures, type coercion, or async behavior, developing a deeper grasp of how JavaScript works under the hood will make you a more confident and capable developer. Keep exploring, keep questioning — that’s how real learning happens.

Top comments (0)