12 Most Tricky JavaScript Interview Questions (ANSWERED)

·19 min read
JavaScriptInterview QuestionsTricky 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.

These 12 questions reveal your mental model of the language—how you think about scope, execution context, type coercion, and the event loop. They consistently separate candidates who understand JavaScript from those who've only used it. For each one, I'll walk through what's really happening and how to demonstrate genuine understanding.

Question 1: The Accidental Global Variable

Let's start with a question that looks innocent but exposes a fundamental misunderstanding about variable declaration:

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

What most candidates say: "Both are undefined—they're inside an IIFE, so they're not accessible outside."

The actual output:

undefined
number

Here's what's really happening. That line var x = y = 5 looks like it's declaring two variables, but it's actually doing something quite different. 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. When interviewers ask this question, they're checking whether you understand both variable scoping and the dangers of implicit globals.

What interviewers are looking for: Candidates who immediately mention that the right-to-left evaluation creates an undeclared variable, and who bring up strict mode as the solution.

Question 2: The Classic Closure Loop Problem

This is perhaps the most famous JavaScript interview question of all time, and for good reason—it tests whether you understand closures and the difference between var and let:

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

What most candidates expect: 0, 1, 2, 3, 4 printed at one-second intervals.

The actual output:

5
5
5
5
5

Each callback logs 5, 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.

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);
}

What interviewers are looking for: They want to hear you explain that var is function-scoped while let is block-scoped, and that closures capture references to variables, not their values at the time of creation.

Question 3: The this Keyword Betrayal

The this keyword in JavaScript is notoriously context-dependent, and this question exposes whether you really understand how it works:

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

The output:

undefined
secret-token

Here's the mental model that will serve you well: this in JavaScript is determined by how a function is called, not where it's defined. Think of the dot before a method call as pointing this at the object on the left.

When you call auth.getSecretToken(), there's a dot before getSecretToken, so this points to auth. But when you assign the function to stealToken and call it as stealToken(), there's no dot—it's just a standalone function call. In non-strict mode, this defaults to the global object (which doesn't have an accessToken property), giving you undefined.

The fix using bind explicitly locks in the this value:

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

Arrow functions offer another solution because they inherit this from their enclosing scope:

const auth = {
    accessToken: 'secret-token',
    getSecretToken: () => this.accessToken  // Caution! 'this' is now outer scope
};

But be careful—this can backfire if the enclosing scope isn't what you expect.

What interviewers are looking for: A clear explanation of how this is determined dynamically at call time, knowledge of bind, call, and apply as solutions, and awareness of how arrow functions handle this differently.

Question 4: Hoisting Madness

Hoisting confuses developers at every level. This question tests whether you understand how function and variable declarations are processed:

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

The output: 99

Wait, didn't we set price = 100? Yes, but not the global price. 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.

Here's another hoisting puzzle that trips people up:

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

The output: 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.

What interviewers are looking for: Understanding that both function declarations and variable declarations are hoisted, but only function declarations are fully hoisted (including their body). Variable declarations are hoisted, but their assignments stay in place.

Question 5: Floating Point Precision

This question reveals whether you understand how numbers work in JavaScript at a fundamental level:

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

The output:

0.30000000000000004
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 areEqual(num1, num2) {
    return Math.abs(num1 - num2) < Number.EPSILON;
}
 
console.log(areEqual(0.1 + 0.2, 0.3)); // true

For financial calculations, you might want to work in cents (integers) and only convert to dollars for display:

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

What interviewers are looking for: Knowledge of IEEE 754 floating-point representation, awareness that this isn't a JavaScript bug but a fundamental limitation of binary representation, and practical solutions for handling it.

Question 6: Event Loop Execution Order

This question tests your understanding of the event loop, which is essential for writing and debugging asynchronous JavaScript:

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

The output:

1
4
3
2

The key insight is that setTimeout(..., 0) doesn't mean "execute immediately." It means "execute as soon as possible, after the current call stack is empty."

Here's the execution flow:

  1. console.log(1) executes synchronously. Output: 1
  2. setTimeout(..., 1000) schedules a callback for ~1 second later
  3. setTimeout(..., 0) schedules a callback for the next tick of the event loop
  4. console.log(4) executes synchronously. Output: 4
  5. Call stack is now empty. Event loop checks the task queue
  6. The 0ms timeout callback runs. Output: 3
  7. After ~1 second, the 1000ms callback runs. Output: 2

Here's a more advanced version that trips up even seasoned developers:

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

The output:

1
4
3
2

The Promise callback (3) runs before the setTimeout callback (2) even though both are asynchronous. This is because Promise callbacks go into the microtask queue, which has higher priority than the macrotask queue (where setTimeout callbacks live).

What interviewers are looking for: Clear understanding of the call stack, task queue, and microtask queue. Bonus points for explaining the difference between macrotasks (setTimeout, setInterval, I/O) and microtasks (Promise.then, queueMicrotask, MutationObserver).

Question 7: Type Coercion Chaos

JavaScript's type coercion can produce surprising results. This question tests whether you understand the rules:

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:

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)

Let me walk through each one:

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 1 + 2 + "3" example shows that evaluation happens left to right. First 1 + 2 gives 3 (number), then 3 + "3" concatenates to "33".

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.

What interviewers are looking for: Awareness that + is overloaded for concatenation, understanding of how == performs type coercion, and knowledge of operator precedence and left-to-right evaluation.

Question 8: The typeof null Bug

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:

object
undefined
false
true

The typeof null === "object" result is one of JavaScript's most famous bugs. In the original implementation, values were stored with a type tag. The tag for objects was 000, and null was represented as the null pointer (0x00), which meant its type tag was also 000. So typeof incorrectly reports it as an object.

This was recognized as a bug early on, but fixing it would have broken too much existing code. A proposal to fix it (TC39's "harmony" proposal) was rejected because it would have been a breaking change.

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."

What interviewers are looking for: Knowledge of this historical bug and why it wasn't fixed (backward compatibility), plus understanding of the semantic difference between null (intentional absence) and undefined (uninitialized).

Question 9: Array Sparse Slots

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:

['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)

What interviewers are looking for: Understanding that arrays are objects with numeric keys, awareness of sparse arrays and their behavior with iteration methods, and knowledge of the difference between "empty" and "undefined."

Question 10: Prototype Chain and delete

This question tests understanding of prototypal inheritance:

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: 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).

What interviewers are looking for: Clear understanding of the prototype chain, knowledge of own properties vs. inherited properties, and awareness of how delete interacts with the prototype chain.

Question 11: Comparison Operator Chaining

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

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

The output:

true
false

Wait, both of these look like they should be true mathematically. The issue is that 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, you need to do it explicitly:

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

What interviewers are looking for: Understanding that JavaScript doesn't support mathematical comparison chaining and awareness of boolean-to-number coercion.

Question 12: The IIFE this Context

This final 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:

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. But inside the IIFE, this is no longer bound to course—it's the global object (or undefined in strict mode).

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 offers cleaner solutions with arrow functions:

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

What interviewers are looking for: Understanding why this changes inside the IIFE, knowledge of the self = this pattern and why it works, and awareness of how arrow functions solve this problem.

What These Questions Really Test

After walking through all 12 questions, you might notice some common themes. Interviewers aren't trying to trick you—they're trying to understand your mental model of JavaScript. Here's what separates candidates who nail these from those who struggle:

Understanding over memorization. Candidates who understand that closures capture references (not values), that this is determined at call time (not definition time), and that JavaScript is single-threaded with an event loop can reason through any variation of these questions. Candidates who memorized answers struggle with slight modifications.

Awareness of the execution model. Knowing that JavaScript hoists declarations, evaluates left to right, and processes the call stack before checking the task queue isn't trivia—it's essential for writing predictable code.

Practical implications. The best candidates don't just explain what happens—they explain why it matters. They mention strict mode when discussing accidental globals. They bring up let vs. var without being prompted. They suggest using Number.EPSILON for floating-point comparisons.

Practice Problems

Here are some additional challenges to test your understanding:

Challenge 1: What does this code output?

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

Challenge 2: What about this one?

console.log([] + []);
console.log([] + {});
console.log({} + []);

Challenge 3: And this async puzzle?

async function test() {
    console.log(1);
    await Promise.resolve();
    console.log(2);
}
 
console.log(3);
test();
console.log(4);

Answers:

Challenge 1: 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.

Challenge 2: "", "[object Object]", and "[object Object]" (or NaN depending on context—if {} is parsed as a code block at the start of a statement). Arrays and objects convert to strings when concatenated with +.

Challenge 3: 3, 1, 4, 2. The async function runs synchronously until the first await, then yields control. The await causes 2 to be scheduled as a microtask.

Quick Reference Table

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 bindingthis is determined by call site, not definition
Function hoistingExecution orderFunction declarations shadow outer variables
0.1 + 0.2Number representationIEEE 754 can't represent all decimals exactly
setTimeout(..., 0)Event loopTask queue runs after call stack clears
'6' + 9 vs '6' - 9Type coercion+ concatenates with strings; - converts
typeof nullHistorical quirksUnfixed bug from JavaScript's first version
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

Wrapping Up

These 12 questions represent the core concepts that experienced JavaScript developers need to internalize. They're not tricks designed to make you fail—they're diagnostic tools that reveal whether you've moved beyond surface-level understanding.

The next time you encounter one of these questions, remember: the interviewer isn't looking for a rehearsed answer. They want to see you reason through the problem, explain the underlying mechanism, and perhaps even discuss how you'd avoid the pitfall in real code.

And if you want to practice with 800+ more interview questions covering JavaScript, React, Angular, TypeScript, and beyond, check out our complete interview prep collection. Each question comes with detailed explanations like the ones in this guide.


Related Articles

If you found this helpful, check out these related guides:

Ready to ace your interview?

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

View PDF Guides