1. Stale closure & primitive capture
What is the output of the following code?
function createIncrement() {
let count = 0;
const message = `Count is ${count}`;
function increment() {
count++;
}
function log() {
console.log(message);
}
return { increment, log };
}
const { increment, log } = createIncrement();
increment();
increment();
log();
Test your understanding of closures, lexical scope, and primitive value capture.
β Output
Count is 0
π§ Explanation
This is a classic stale closure trap β but not in the way most developers expect.
Step-by-step execution:
-
createIncrement()is invoked β new lexical environment created:-
count = 0(mutable binding) -
message = "Count is 0"(primitive string, interpolated immediately at assignment)
-
- Inner functions
incrementandlogare defined. Both close over the same lexical environment. -
increment()is called twice:-
countmutates:0 β 1 β 2β - This works as expected.
-
-
log()is called:- It references the variable
message -
messagestill holds the original string value"Count is 0" - The template literal was evaluated once, at the moment of assignment β not re-evaluated when
log()runs.
- It references the variable
π Core Concept
> Closures capture variables, not expressions.
> But if a variable holds a primitive value (string, number, boolean), that value is fixed at assignment time.
message is not a live reference to count. It is a snapshot.
π How to fix it (if dynamic output is desired)
Re-evaluate the template literal inside log():
function log() {
console.log(`Count is ${count}`);
}
π― What this question tests
| Concept | Why it matters |
|---|---|
| Template literal evaluation timing | They run at assignment, not at access |
| Primitive vs reference types | Primitives are copied by value; objects/arrays are referenced |
| Closure capture semantics | Closures close over bindings, but the value of a primitive is immutable once assigned |
| Mental model of "live" variables | Not all variables in a closure are "live views" β only the bindings themselves are |
2. JavaScript context trap: losing this
What will this code output when user.greet() is called?
const user = {
name: "Alex",
greet() {
console.log(`Hello, ${this.name}!`);
const innerNormal = function () {
console.log(`Normal: ${this.name}`);
};
const innerArrow = () => {
console.log(`Arrow: ${this.name}`);
};
innerNormal();
innerArrow();
},
};
user.greet();
Test your understanding of execution context, function invocation patterns, and arrow function lexical binding.
β Output
Hello, Alex!
Normal: undefined
Arrow: Alex
π§ Explanation
This question tests three different ways this is resolved in JavaScript:
1. Method call: greet()
- Called as
user.greet() -
thisis bound to the object preceding the dot βuser - Output:
Hello, Alex!
2. Regular function: innerNormal()
- Called as a standalone function:
innerNormal() - No object precedes the call. In strict mode (default in modern JS/modules),
thisisundefined. In non-strict browser environments, it falls back towindow. -
undefined.nameevaluates toundefined(orwindow.namewhich is typically"") - Output:
Normal: undefined
3. Arrow function: innerArrow()
- Arrow functions do not have their own
thisbinding - They capture
thislexically from the enclosing execution context at definition time - The enclosing context is
greet(), wherethis === user - Output:
Arrow: Alex
π Core Concept
> Regular functions resolve this at call time (dynamic binding).
> Arrow functions capture this at definition time (lexical binding).
π How to fix innerNormal if you want it to see user
// Option 1: Explicit binding at call time
innerNormal.call(this);
// Option 2: Explicit binding at definition time
const innerNormal = function () {
console.log(this.name);
}.bind(this);
// Option 3: Lexical capture via closure (older pattern)
const self = this;
const innerNormal = function () {
console.log(self.name);
};
π― What this question tests
| Concept | Why it matters |
|---|---|
Implicit this binding |
Regular functions lose context when called without an explicit receiver |
Lexical this capture |
Arrow functions inherit this from their parent scope |
| Strict vs non-strict mode | Changes fallback behavior (undefined vs global object) |
| Mental model of execution context |
this is dynamic for regular functions, static for arrows |
3. Illusion of an "own" property on mutation
What will the code log to the console at the end?
const grandparent = {
heritage: ["gold", "land"],
coins: 100,
};
const parent = Object.create(grandparent);
const child = Object.create(parent);
child.coins += 50;
child.heritage.push("debts");
console.log(grandparent.coins); // (1) ?
console.log(grandparent.heritage); // (2) ?
console.log(child.coins); // (3) ?
console.log(child.heritage); // (4) ?
Test your understanding of the difference between reading a property from the prototype chain and writing a property on an object.
β Output
100
["gold", "land", "debts"]
150
["gold", "land", "debts"]
π§ Explanation
This is the mental trap:
For coins
The expression child.coins += 50 desugars to child.coins = child.coins + 50.
- JavaScript reads
child.coins. It is not found onchild, so the engine walks the prototype chain tograndparentand reads100. - It adds
50β150. - It writes the result to
child.coins.
Writes always land on the object that received the assignment. child gets its own coins: 150, while grandparent.coins stays 100.
For heritage
The expression child.heritage.push("debts") does not assign to heritage.
- JavaScript reads
child.heritage, finds the array ongrandparent, and keeps a reference to that same array in the heap. -
.push("debts")mutates that shared array.
child never gets an own heritage property β it still resolves to the grandparent's array, which now includes "debts".
π Core Concept
> Read vs write behave differently on the prototype chain.
> Assignment creates (or updates) an own property on the target object.
> Mutating a referenced object affects the shared value visible to every object in the chain that points to it.
π How to avoid accidental prototype mutation
// Option 1: Assign a new array instead of mutating the inherited one
child.heritage = [...child.heritage, "debts"];
// Option 2: Copy mutable values when creating the child
const child = Object.create(parent);
Object.assign(child, {
coins: grandparent.coins,
heritage: [...grandparent.heritage],
});
child.coins += 50;
child.heritage.push("debts"); // now only child's copy is affected
π― What this question tests
| Concept | Why it matters |
|---|---|
| Prototype property lookup | Reads fall through the chain until a property is found |
| Assignment vs mutation |
+= writes locally; .push() mutates a shared reference |
| Own vs inherited properties |
hasOwnProperty and debugging surprises depend on this distinction |
| Reference types on prototypes | Shared mutable state on a prototype affects all descendants |
4. JavaScript coercion & precedence trap
What will this code output?
console.log(+"5" + [1] + !"0");
Test your understanding of operator precedence, implicit type conversion, and left-to-right evaluation.
β Output
"51false"
π§ Explanation
This expression combines unary operators, object-to-primitive coercion, and left-to-right associativity. Here's the exact execution flow:
Step 1: Unary operators (highest precedence)
Unary + and ! are evaluated before binary +.
-
+"5"βToNumberconversion β5 -
!"0"β"0"is truthy β!trueβfalse - Expression becomes:
5 + [1] + false
Step 2: Left-to-right evaluation & first +
Binary + is left-associative. It evaluates 5 + [1] first.
- One operand is a
Number, the other is anObject. - JS invokes
ToPrimitive([1])β callsArray.toString()β"1" - Since one operand is now a string,
+switches to string concatenation -
"5" + "1"β"51" - Expression becomes:
"51" + false
Step 3: Second + & final coercion
-
"51"(string) +false(boolean) - Boolean
falseis coerced to string β"false" - Concatenation:
"51" + "false"β"51false"
π Core Concept
> Precedence decides what runs first.
> Associativity decides direction (left β right for +).
> Coercion decides how types interact when mismatched.
The + operator is unique in JS: it performs both addition and concatenation. If either operand becomes a string during ToPrimitive, the entire operation switches to string concatenation.
π Common pitfalls to avoid
| Mistake | Why it's wrong |
|---|---|
Thinking 5 + [1] equals 6
|
[1] is not a number; it's coerced to "1"
|
Thinking !"0" equals true
|
Only "", 0, null, undefined, NaN are falsy. "0" is a non-empty string β truthy |
| Assuming right-to-left evaluation |
+ is strictly left-associative in JS |
π― What this question tests
| Concept | Why it matters |
|---|---|
| Operator precedence table | Determines evaluation order before execution begins |
ToPrimitive & ToString algorithms |
How objects/arrays convert in mixed-type expressions |
| Left-to-right associativity | Critical for chaining + operations with side effects or type shifts |
| Falsy/truthy rules | Essential for !, &&, ` |
5. JavaScript event loop & queue priority trap
In what order will the numbers be printed to the console?
console.log("1");
setTimeout(() => {
console.log("2");
Promise.resolve().then(() => {
console.log("3");
});
}, 0);
new Promise((resolve) => {
console.log("4");
resolve();
}).then(() => {
console.log("5");
setTimeout(() => {
console.log("6");
}, 0);
});
console.log("7");
Test your understanding of the call stack, microtask queue, and macrotask queue execution order.
β Output
1
4
7
5
2
3
6
π§ Explanation
This question tests the exact execution order defined by the JavaScript event loop.
Step 1: Synchronous execution (call stack)
-
console.log("1")runs immediately β outputs1 -
setTimeoutschedules its callback as a macrotask and exits -
new Promiseexecutor runs synchronously βconsole.log("4")outputs4.resolve()is called -
.then()schedules its callback as a microtask -
console.log("7")runs immediately β outputs7 - Call stack is now empty
Step 2: Microtask queue (priority over macrotasks)
- The event loop checks the microtask queue before picking the next macrotask
- Microtask 1 runs:
console.log("5")β outputs5 - Inside it,
setTimeoutschedules a new callback as macrotask 2 (appended to the macrotask queue) - Microtask queue is now empty
Step 3: Macrotask queue (first cycle)
- Event loop picks macrotask 1 (the first
setTimeout) -
console.log("2")β outputs2 -
Promise.resolve().then(...)schedules microtask 2
Step 4: Microtask queue (again)
- Before moving to the next macrotask, the event loop must completely drain the microtask queue
- Microtask 2 runs:
console.log("3")β outputs3 - Microtask queue is empty
Step 5: Macrotask queue (second cycle)
- Event loop picks macrotask 2 (the second
setTimeout) -
console.log("6")β outputs6
π Core Concept
> Execution order: synchronous code β microtasks (Promise, queueMicrotask) β macrotasks (setTimeout, setInterval, I/O) β repeat.
>
> Every time a macrotask finishes, the event loop must completely drain the microtask queue before proceeding to the next macrotask. Nested microtasks created during a macrotask will delay the next macrotask.
π― What this question tests
| Concept | Why it matters |
|---|---|
| Promise executor timing | Runs synchronously during construction, not asynchronously |
| Microtask vs macrotask priority | Microtasks always clear before the next macrotask runs |
| Nested async scheduling | New tasks are appended to the back of their respective queues |
| Event loop phases | Critical for debugging race conditions, UI freezes, and API batching |
6. Microtask order: async/await vs promise chains
In what exact order will the logs be printed to the console?
async function asyncFunc() {
console.log("2");
await Promise.resolve();
console.log("3");
}
console.log("1");
asyncFunc();
Promise.resolve()
.then(() => {
console.log("4");
})
.then(() => {
console.log("5");
});
console.log("6");
Test your understanding of how
awaitis compiled under the hood and its exact scheduling priority compared to.then().
β Output
1
2
6
3
4
5
π§ Explanation
This question reveals exactly how async/await is translated into promise chains and how the event loop schedules continuations.
Step 1: Synchronous phase
-
console.log("1")executes β outputs1 -
asyncFunc()is invoked. Code runs synchronously until the firstawait -
console.log("2")executes β outputs2 -
await Promise.resolve()is encountered. The expression on the right evaluates to an already-resolved promise. The engine wraps the remaining function body (console.log("3")) in a microtask and suspends execution. This microtask is queued - Control returns to the global scope
-
Promise.resolve().then(...)registers a callback. This creates microtask 2 (logs4). The chained.then(logs5) is not queued yet; it waits for microtask 2 to resolve -
console.log("6")executes β outputs6 - Call stack is empty. Event loop switches to the microtask queue
Step 2: Microtask queue processing (FIFO order)
-
Microtask 1 (from
awaitcontinuation): runsconsole.log("3")β outputs3 -
Microtask 2 (from first
.then): runsconsole.log("4")β outputs4. Its resolution immediately queues the next.thencallback as microtask 3 -
Microtask 3 (from chained
.then): runsconsole.log("5")β outputs5 - Microtask queue is empty
π Core Concept
awaitis syntactic sugar for.then().
The code afterawaitis compiled into a.then()callback and scheduled as a microtask.
Microtasks are processed in strict FIFO order based on when they were registered during synchronous execution.
Want more? Iβm maintaining a curated repository with tricky JS questions and edge cases. Check the full list here: [https://github.com/Skillhacker-io/javascript-interview-questions/blob/main/questions/top-javascript-questions.md]
United States
NORTH AMERICA
Related News
Every Medium Publication That Accepts 3D Content (2026 Map)
13h ago

Agentic Ops: How I Shipped My Vibe-Coded Game to Production
14h ago
I build a project calculator web app for n8n / automation folks
14h ago
Integers and Floating-Point Numbers in C++
14h ago

How to Secure Azure Storage Using Managed Identities and RBAC
14h ago