17 Tricky JavaScript Interview Questions (2026)

·17 min read
By ·Updated
javascriptinterview-questionsclosureshoistingevent-looptype-coercionfrontend

Why does typeof null return "object"? Why does 0.1 + 0.2 not equal 0.3? Why does a simple for-loop with setTimeout print the same number five times? These aren't obscure edge cases—they're deliberate interview questions designed to expose whether you've truly wrestled with JavaScript's quirks or just memorized syntax. This guide covers the tricky questions that separate candidates who understand JavaScript from those who've only used it.

Table of Contents

  1. Variable Scope Questions
  2. Closure Questions
  3. this Keyword Questions
  4. Type Coercion Questions
  5. Event Loop Questions
  6. Number and Array Questions
  7. Prototype Questions
  8. Quick Reference

Variable Scope Questions

These questions test your understanding of how JavaScript handles variable declarations and hoisting.

What happens with var x = y = 5 inside an IIFE?

This question looks innocent but exposes a fundamental misunderstanding about variable declaration:

(function () {
    var x = y = 5;
})();
console.log(typeof x);
console.log(typeof y);

The output is undefined for x and number for y. That line var x = y = 5 looks like it's declaring two variables, but JavaScript interprets this as:

y = 5;        // Assignment without declaration - creates global!
var x = y;    // Local variable declaration and assignment

The var keyword only applies to x. The y is assigned without any declaration keyword, which in non-strict mode means JavaScript creates it as a property on the global object. After the IIFE executes, x is gone (it was function-scoped), but y is sitting there on the global object with the value 5.

This is why strict mode exists. If you add "use strict"; at the top, this code throws a ReferenceError because assigning to an undeclared variable becomes an error.

Why does this hoisting example output 99?

Hoisting confuses developers at every level. Consider this code:

var price = 99;
 
function calculatePrice() {
    price = 100;
    return;
 
    function price() {}
}
 
calculatePrice();
console.log(price);

The output is 99, not 100. Here's what JavaScript actually sees after hoisting:

var price = 99;
 
function calculatePrice() {
    // Hoisted function declaration creates a local 'price' variable
    function price() {}
 
    price = 100;  // Modifies the LOCAL price, not the global one
    return;
}
 
calculatePrice();
console.log(price);  // Global price is unchanged: 99

The function declaration function price() {} gets hoisted to the top of calculatePrice, creating a local variable named price that shadows the global one. When we assign 100 to price, we're modifying this local variable, leaving the global price at 99.

Why does this variable declaration log undefined?

Here's another hoisting puzzle that trips people up:

var price = 10;
var getCoursePrice = function () {
    console.log(price);
    var price = 100;
};
getCoursePrice();

The output is undefined. Even though there's a global price set to 10, the local var price = 100 declaration is hoisted to the top of the function (but not its initialization). The function sees a local price that exists but hasn't been assigned yet.

This is var declaration instantiation: the local binding exists and is initialized to undefined before the function body runs, while the assignment still occurs at its original line. It is not a Temporal Dead Zone. TDZ behavior applies to lexical declarations such as let, const, and class, whose bindings exist but cannot be accessed before initialization.


Closure Questions

Closures are fundamental to JavaScript. These questions test whether you understand how they capture variables.

Why does the classic for-loop with setTimeout print 5 five times?

This is perhaps the most famous JavaScript interview question of all time:

for (var i = 0; i < 5; i++) {
    setTimeout(() => console.log(i), i * 1000);
}

The output is 5, 5, 5, 5, 5—each printed roughly one second apart.

Think of closures like a photograph that captures references, not values. When each setTimeout callback is created, it doesn't take a snapshot of i's current value. Instead, it holds a reference to the same variable i that the loop is modifying. By the time any callback actually executes (even the one with 0ms delay, since the call stack must clear first), the loop has finished and i has been incremented to 5.

How do you fix the closure loop problem?

The fix with let is elegant because let creates block scope. Each iteration of the loop gets its own distinct i variable:

for (let i = 0; i < 5; i++) {
    setTimeout(() => console.log(i), i * 1000);
}
// Output: 0, 1, 2, 3, 4

Before ES6 introduced let, developers solved this with an IIFE that captured the value in a new scope:

for (var i = 0; i < 5; i++) {
    (function(capturedI) {
        setTimeout(() => console.log(capturedI), capturedI * 1000);
    })(i);
}

The key insight is that var is function-scoped while let is block-scoped, and closures capture references to variables, not their values at the time of creation.


this Keyword Questions

The this keyword in JavaScript is notoriously context-dependent. These questions expose whether you really understand how it works.

Why does a detached method call return undefined?

Consider this code:

const auth = {
    accessToken: 'secret-token',
    getSecretToken: function () {
        return this?.accessToken;
    }
};
 
const stealToken = auth.getSecretToken;
console.log(stealToken());
console.log(auth.getSecretToken());

The output is undefined then secret-token. Optional chaining makes the first result deterministic even when a standalone call has this === undefined.

For ordinary functions, this is usually determined by how the function is called, not where it is defined. Arrow functions are the important exception: they capture this lexically from the surrounding code.

When you call auth.getSecretToken(), the reference used for the call supplies auth as this. After detaching the function, stealToken() is a plain call: this is undefined in strict mode and ECMAScript modules, or globalThis in a sloppy classic script. Without ?., the strict-mode version would throw while reading accessToken.

How do you fix the detached method problem?

The fix using bind explicitly locks in the this value:

const stealToken = auth.getSecretToken.bind(auth);
console.log(stealToken()); // 'secret-token'

An arrow wrapper can preserve the intended receiver by making the method call explicitly:

const stealToken = () => auth.getSecretToken();

Do not replace the method itself with an arrow property merely to "fix" this: an arrow captures this from the surrounding scope and does not receive the object before the dot as its receiver.

What does this output inside an IIFE?

This question combines closures, this binding, and IIFE patterns:

const course = {
    title: "react",
    printDetails: function () {
        const self = this;
        console.log("a = " + this.title);
        console.log("b = " + self.title);
        (function () {
            console.log("c = " + this?.title);
            console.log("d = " + self.title);
        }());
    }
};
course.printDetails();

The output is:

a = react
b = react
c = undefined
d = react

The self = this pattern is an old-school solution to the "disappearing this" problem. Inside printDetails, both this and self refer to course. The plain IIFE call does not supply course as a receiver: this is globalThis in sloppy scripts or undefined in strict code. Optional chaining keeps the displayed output the same in either case.

However, self is just a regular variable that closes over the outer scope, so it still holds a reference to the course object. That's why d correctly logs "react" while c is undefined.

Modern JavaScript solves this with arrow functions:

const course = {
    title: "react",
    printDetails: function () {
        console.log("a = " + this.title);
        (() => {
            console.log("c = " + this.title);  // Arrow function inherits 'this'
        })();
    }
};

Type Coercion Questions

JavaScript's type coercion can produce surprising results. These questions test whether you understand the rules.

What are the outputs of these type coercion expressions?

console.log('false' == false);
console.log(2 + true);
console.log('6' + 9);
console.log('6' - 9);
console.log(1 + 2 + "3");
console.log(2 in [1, 2]);

The outputs are:

false    // String 'false' is not equal to boolean false
3        // true is coerced to 1
'69'     // + with a string concatenates
-3       // - with a string converts to number
'33'     // 1 + 2 = 3, then 3 + "3" = "33"
false    // 2 is not an index in [1, 2] (indices are 0 and 1)

The 'false' == false comparison is tricky. You might think "the string 'false' represents false, so they're equal." But JavaScript doesn't work that way. With ==, the operands are converted for comparison. false becomes 0, and 'false' becomes NaN (because it's not a numeric string). 0 != NaN, so the result is false.

The + operator has dual behavior: it adds numbers but concatenates strings. When one operand is a string, the other is converted to a string. That's why '6' + 9 gives '69' but '6' - 9 gives -3 (the - operator only works with numbers, so '6' is converted to 6).

The in operator checks for property names (including array indices), not values. The array [1, 2] has indices 0 and 1, so 2 in [1, 2] is false.

Why does typeof null return 'object'?

This question tests your knowledge of JavaScript's historical quirks:

console.log(typeof null);
console.log(typeof undefined);
console.log(null === undefined);
console.log(null == undefined);

The output is:

object
undefined
false
true

The typeof null === "object" result is a standardized legacy quirk. The current ECMAScript algorithm explicitly returns "object" for null; it does not mean that null has object properties or an object prototype. Use value === null to test for it.

The difference between null === undefined (false) and null == undefined (true) is important. With strict equality, they're different types. With loose equality, JavaScript considers them "loosely equal" because both represent "absence of value."

Why does 1 < 2 < 3 return true but 3 > 2 > 1 return false?

This question looks like simple math but exposes a JavaScript quirk:

console.log(1 < 2 < 3);
console.log(3 > 2 > 1);

The output is true then false.

JavaScript doesn't support chained comparisons like Python does. Instead, it evaluates left to right with type coercion.

For 1 < 2 < 3:

  1. 1 < 2 evaluates to true
  2. true < 31 < 3 (true is coerced to 1) → true

For 3 > 2 > 1:

  1. 3 > 2 evaluates to true
  2. true > 11 > 1 (true is coerced to 1) → false

If you actually want to check if a value is between two bounds:

const x = 2;
console.log(1 < x && x < 3);  // true - proper range check

Event Loop Questions

These questions test your understanding of the event loop, which is essential for writing and debugging asynchronous JavaScript.

What is the execution order of this setTimeout code?

(function () {
    console.log(1);
    setTimeout(() => console.log(2), 1000);
    setTimeout(() => console.log(3), 0);
    console.log(4);
})();

The output is 1, 4, 3, 2.

The key insight is that setTimeout(..., 0) does not mean "execute immediately." It registers a timer with a minimum delay; after the threshold, the callback becomes a task that still waits for the event loop to select it.

Here's the execution flow:

  1. console.log(1) executes synchronously. Output: 1
  2. setTimeout(..., 1000) registers a timer whose callback cannot run before its threshold
  3. setTimeout(..., 0) registers a timer task with the minimum applicable delay
  4. console.log(4) executes synchronously. Output: 4
  5. The current task finishes and the microtask checkpoint completes
  6. The 0ms timeout callback runs. Output: 3
  7. After ~1 second, the 1000ms callback runs. Output: 2

Why do Promises execute before setTimeout even with 0 delay?

Here's a more advanced version:

console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');

The output is 1, 4, 3, 2.

The Promise reaction (3) runs before the timer callback (2) in this example because both are scheduled during the same task. The reaction enters the microtask queue, while the timer callback can only run as a later timer task.

When the current task ends, the browser drains the microtask queue before selecting another task. This describes ordering within the example; it is not a universal statement that any Promise callback in a program precedes any timer or I/O callback regardless of when each was queued.


Number and Array Questions

These questions test your understanding of JavaScript's number representation and array internals.

Why does 0.1 + 0.2 not equal 0.3?

console.log(0.1 + 0.2);
console.log(0.1 + 0.2 === 0.3);

The output is 0.30000000000000004 and false.

JavaScript uses IEEE 754 double-precision floating-point format for all numbers. The problem is that certain decimal fractions, like 0.1 and 0.2, cannot be represented exactly in binary. It's similar to how 1/3 can't be represented exactly in decimal (0.333...).

When you add two slightly imprecise numbers, the small errors accumulate, giving you 0.30000000000000004 instead of 0.3.

The practical solution is to compare floating-point numbers using a tolerance:

function areNearlyEqual(num1, num2, relativeTolerance = Number.EPSILON) {
    const scale = Math.max(1, Math.abs(num1), Math.abs(num2));
    return Math.abs(num1 - num2) <= relativeTolerance * scale;
}
 
console.log(areNearlyEqual(0.1 + 0.2, 0.3)); // true

For ordinary monetary amounts, integer minor units avoid binary-fraction surprises. Large values, currencies with different minor units, or exact accounting rules may require BigInt or a decimal library:

const priceInCents = 1099;  // $10.99
const taxInCents = 88;       // $0.88
const total = (priceInCents + taxInCents) / 100; // $11.87, no precision issues

What is the difference between empty slots and undefined in arrays?

This question reveals understanding of how arrays actually work in JavaScript:

const courses = ['React'];
courses[5] = 'Angular';
courses[4] = undefined;
console.log(courses);
console.log(courses[3]);
console.log(courses.map(c => 'Vue'));

The output is:

['React', <3 empty items>, undefined, 'Angular']
undefined
['Vue', <3 empty items>, 'Vue', 'Vue']

There's a crucial difference between an "empty slot" and a slot containing undefined. When you set courses[5] = 'Angular' without setting indices 1-4, those positions are empty slots (also called "holes"). They're not undefined—they literally don't exist.

When you access courses[3], JavaScript returns undefined because there's nothing there. But here's the key insight: array methods like map, filter, and forEach skip over empty slots entirely. They only iterate over slots that actually have values (including undefined if explicitly set).

That's why courses[4] (which we explicitly set to undefined) gets mapped to 'Vue', but indices 1-3 remain empty in the result.

You can check for empty slots using:

console.log(3 in courses);  // false (empty slot)
console.log(4 in courses);  // true (has value: undefined)

Prototype Questions

These questions test understanding of prototypal inheritance.

What happens when you delete a property that shadows a prototype property?

const reactCourse = {
    price: 59.99,
    getPrice: function () {
        return this.price;
    }
};
 
const vueCourse = Object.create(reactCourse);
vueCourse.price = 69.99;
delete vueCourse.price;
console.log(vueCourse.getPrice());

The output is 59.99.

Here's what's happening step by step:

  1. Object.create(reactCourse) creates a new object with reactCourse as its prototype
  2. vueCourse.price = 69.99 creates an "own property" on vueCourse, shadowing the inherited one
  3. delete vueCourse.price removes only the own property
  4. Now when getPrice() is called, it looks for price on vueCourse, doesn't find it (we deleted the own property), and walks up the prototype chain to find price on reactCourse

The delete operator only affects own properties, not inherited ones. If you want to completely remove access to the inherited price, you'd need to delete it from reactCourse itself (which would affect all objects inheriting from it).

How do reference assignments work with objects?

let a = { value: 1 };
let b = a;
a.value = 2;
console.log(b.value);
a = { value: 3 };
console.log(b.value);

The output is 2 then 2.

Objects are assigned by reference. b and a initially point to the same object, so changing a.value affects b. But a = { value: 3 } creates a new object and points a to it, leaving b pointing to the original object (which still has value: 2).


Quick Reference

ConceptWhat It TestsKey Insight
var x = y = 5Implicit globalsRight-to-left assignment creates undeclared global
Closure loop with varClosures & scopevar is function-scoped; closures capture references
Detached method callthis bindingOrdinary functions use call-site binding; arrows capture lexical this
Function hoistingExecution orderFunction declarations shadow outer variables
0.1 + 0.2Number representationIEEE 754 can't represent all decimals exactly
setTimeout(..., 0)Event loopZero is a minimum delay; the timer callback runs in a later task
'6' + 9 vs '6' - 9Type coercion+ concatenates with strings; - converts
typeof nullHistorical quirksStandardized legacy result; test null with strict equality
Sparse arraysArray internalsEmpty slots are different from undefined values
delete with prototypesPrototype chainOnly removes own properties, not inherited
1 < 2 < 3Operator evaluationNo chaining; left-to-right with coercion
IIFE this contextClosures & thisIIFE creates new execution context

Official Sources

Frequently Asked Questions

What are the most common tricky JavaScript interview questions?

The most common tricky JavaScript questions test closures in loops, hoisting behavior with var/let, type coercion quirks like 0.1 + 0.2, the 'this' keyword in different contexts, event loop execution order with setTimeout vs Promises, and implicit global variables in IIFE patterns.

Why does typeof null return 'object' in JavaScript?

The ECMAScript typeof operator explicitly returns 'object' for null. It is a standardized legacy quirk kept for web compatibility, not evidence that null is an object. Test value === null when you need to identify null.

How do you explain the classic closure loop problem in interviews?

With 'var', the variable is function-scoped, so all callbacks share the same variable. By the time callbacks execute, the loop has finished and the variable holds its final value. Using 'let' creates a new binding per iteration, preserving each value. This demonstrates understanding of scope and closures.

Why does 0.1 + 0.2 not equal 0.3 in JavaScript?

JavaScript Number values use IEEE 754 binary64, which cannot represent 0.1 or 0.2 exactly, so their sum is 0.30000000000000004. Compare within a tolerance scaled to the magnitude of the operands; Number.EPSILON alone is only an appropriate absolute tolerance near 1. Use integer minor units or a decimal library for money.

What happens when you use setTimeout with 0 delay?

The callback is not executed immediately. setTimeout registers a timer and, after at least the delay threshold, queues a timer task. The current task must finish and its microtask checkpoint must complete before the event loop can select that timer task. A zero delay is a minimum threshold, not an execution guarantee.

How does JavaScript's type coercion affect comparisons?

With loose equality (==), JavaScript converts operands to matching types before comparison. This leads to surprising results like '1' == 1 being true, or [] == false being true. Using strict equality (===) avoids coercion and checks both value and type.

Ready to ace your interview?

Get 550+ interview questions with detailed answers in our comprehensive PDF guides.

View PDF Guides