ForgeFrontend - Prepare, Practice, Crack
Secure checkout
Lifetime access
Instant PDF download
Free updates forever

Prepare · Practice · Crack

JavaScript Output-Based Interview Questions

Short answer

Output questions test whether you can predict what JavaScript does, not whether you can write it. Each snippet below comes with its exact output and the one rule that produces it, grouped by the model being tested so you can tell which one you misread.

FamilyThe giveaway in the snippetThe rule that decides it
Hoisting / TDZA read above a declarationvar is undefined; let and const throw
Scope / closuresA loop with a deferred callbackvar has one binding; let has one per iteration
Event loopsetTimeout next to a .thenSync, then all microtasks, then one macrotask
this bindingA method assigned to a variableThe call site decides, except in arrows
Coercion== , or + with mixed types+ prefers strings; everything else prefers numbers
ReferencesSpread, or an object as a keyCopies are shallow; keys are stringified
Every output question is one of these six. Identify the family first and the answer follows.

How to answer an output question

Read the snippet and name the family before you predict anything. Almost every output question is deliberately built to test exactly one of the six above, and once you have identified which, you are applying a rule rather than simulating the whole program in your head. That is also what protects you when the interviewer changes one line and asks again.

Then say the answer and the rule together. "Three, three, three, because var has one binding for the whole function and all three callbacks read it after the loop finished" is a complete answer that pre-empts the follow-up. Just saying "3 3 3" invites it, and a correct answer with no reasoning behind it is worth surprisingly little, because the interviewer cannot tell it from a memorised one.

Hoisting and the temporal dead zone

Declarations are processed before any code runs. `var` bindings are created and initialised to undefined; `let` and `const` bindings are created but left uninitialised, so touching them throws. Function declarations are hoisted whole and callable before their line; function expressions are not.

Four hoisting snippetsjavascript
1console.log(a);
2var a = 1;
3// → undefined      declared and initialised to undefined, not yet assigned
4
5// console.log(b);
6// let b = 2;
7// → ReferenceError: Cannot access 'b' before initialization
8
9fn();
10function fn() { console.log("hoisted whole"); }
11// → hoisted whole
12
13// expr();
14// var expr = function () {};
15// → TypeError: expr is not a function     the VAR hoisted, the value did not
16
17var x = 1;
18function shadow() {
19  console.log(x);
20  var x = 2;
21}
22shadow();
23// → undefined      the inner var shadows the outer from the top of the
24//                  function, so the outer x is unreachable here

The last one is the version interviewers actually use, because it looks like it should print 1. The inner `var x` is hoisted to the top of the function, which shadows the outer binding for the whole function body, so the read happens against a variable that exists but has not been assigned yet. Swap `var` for `let` and it throws instead, which is the follow-up.

Scope and closures

A closure captures a live reference to a binding, not a copy of its value. The whole family of loop questions reduces to counting how many bindings exist: one for the entire function with `var`, one per iteration with `let`.

Loops, and the version without a timerjavascript
1for (var i = 0; i < 3; i++) setTimeout(() => console.log(i));
2// → 3
3// → 3
4// → 3
5
6for (let j = 0; j < 3; j++) setTimeout(() => console.log(j));
7// → 0
8// → 1
9// → 2
10
11const fns = [];
12for (var k = 0; k < 3; k++) fns.push(() => k);
13console.log(fns.map((f) => f()));
14// → [ 3, 3, 3 ]      no timers involved: it was never about async
15
16function counter() {
17  let n = 0;
18  return { inc: () => ++n, get: () => n };
19}
20const c1 = counter(), c2 = counter();
21c1.inc(); c1.inc();
22console.log(c1.get(), c2.get());
23// → 2 0              each call to counter() creates a NEW binding

The third snippet is the one to have ready, because it removes `setTimeout` entirely and proves the behaviour has nothing to do with asynchrony. The fourth is the reverse question, two independent closures, and it catches people who have over-learnt "closures share state" from the loop example without noticing that each invocation creates its own scope.

Event loop ordering

Synchronous code runs to completion first. Then the entire microtask queue drains, promise callbacks and `queueMicrotask`, and only then does one macrotask run, such as a timer. After each macrotask the microtask queue drains again.

The standard ordering question, and the async-function twistjavascript
1console.log("1");
2setTimeout(() => console.log("2"), 0);
3Promise.resolve().then(() => console.log("3"));
4console.log("4");
5// → 1
6// → 4
7// → 3
8// → 2
9
10async function run() {
11  console.log("A");
12  await null;
13  console.log("B");
14}
15console.log("start");
16run();
17console.log("end");
18// → start
19// → A            the body runs SYNCHRONOUSLY until the first await
20// → end
21// → B            everything after await is a microtask
22
23setTimeout(() => {
24  console.log("timer");
25  Promise.resolve().then(() => console.log("micro inside timer"));
26}, 0);
27setTimeout(() => console.log("second timer"), 0);
28// → timer
29// → micro inside timer      microtasks drain BETWEEN macrotasks
30// → second timer

The second snippet is where most people lose the mark. Calling an async function does not defer anything, the body executes immediately up to the first `await`, so "A" prints before "end". The third shows the rule that follows from it: the microtask queue is emptied between every pair of macrotasks, not just once at the start.

this binding

In a normal function, `this` is whatever precedes the dot at the call site, nothing else. Detaching a method loses it. Arrow functions have no `this` of their own and use the enclosing scope's, which fixes callbacks and breaks methods.

Losing the receiver, and the arrow-as-method trapjavascript
1const obj = {
2  name: "obj",
3  regular() { return this?.name; },
4  arrow: () => this?.name,
5};
6
7console.log(obj.regular());        // → obj
8console.log(obj.arrow());          // → undefined   captured module scope
9
10const detached = obj.regular;
11console.log(detached());           // → undefined   no object before the dot
12console.log(detached.call(obj));   // → obj
13
14const timer = {
15  name: "timer",
16  startBroken() { setTimeout(function () { console.log(this?.name); }, 0); },
17  startFixed()  { setTimeout(() => console.log(this?.name), 0); },
18};
19timer.startBroken();               // → undefined   plain call inside setTimeout
20timer.startFixed();                // → timer       arrow keeps the method's this
21
22const nested = {
23  name: "nested",
24  run() {
25    function inner() { return this?.name; }
26    return inner();
27  },
28};
29console.log(nested.run());         // → undefined   inner was called plainly

The last snippet is the one that produces the most wrong answers, because the inner function is written inside a method and people assume it inherits from it. It does not - `inner()` is a plain call with nothing before the dot, so it gets the default binding. Making `inner` an arrow function is the fix, and being able to say why in one sentence is the whole point of the question.

This is 1 of 80+ questions in the JavaScript Interview Kit

Get every question with detailed answers, follow-ups and real code, plus coding challenges and a last-minute revision sheet. One-time payment, instant access.

⚡ Get the JavaScript Interview Kit → ₹299

Coercion and equality

`+` prefers string concatenation whenever either operand is a string; every other arithmetic operator forces both sides to numbers. `==` converts before comparing, following rules that produce results almost nobody predicts, which is exactly why these appear in interviews.

The operator rules, and the famous [] == ![]javascript
1console.log(1 + "2");        // → 12      + with a string concatenates
2console.log(1 + "2" - 1);    // → 11      then - forces numbers: 12 - 1
3console.log("5" * "2");      // → 10      * has no string behaviour
4console.log(1 + 2 + "3");    // → 33      left to right: 3, then "33"
5console.log("1" + 2 + 3);    // → 123     left to right: "12", then "123"
6
7console.log([] == ![]);
8// → true
9// ![] is false → [] == false → [] == 0 → "" == 0 → 0 == 0
10
11console.log([] + {});        // → [object Object]
12console.log(null + 1);       // → 1       null becomes 0
13console.log(undefined + 1);  // → NaN     undefined becomes NaN
14console.log(true + true);    // → 2
15
16console.log([1, 2] + [3]);   // → 1,23    both join(",") first
17console.log(typeof (1 + "1"));  // → string

The `1 + 2 + "3"` versus `"1" + 2 + 3` pair is worth memorising as a unit, because it demonstrates the only thing that actually matters here: operators evaluate left to right, so where the string appears changes everything. And the right way to close any of these is to say you would use `===` and never write code that depends on the coercion rules.

References, copies and object keys

Objects are held by reference, `const` locks the binding rather than the value, and spreading copies only the top level. Together those three facts generate a large share of output questions and an even larger share of real bugs.

Shallow copies, const, and stringified keysjavascript
1const o = { a: 1, nested: { b: 2 } };
2o.a = 99;                    // fine, mutating the object
3// o = {};                   // → TypeError: Assignment to constant variable
4console.log(o.a);            // → 99
5
6const copy = { ...o };
7copy.nested.b = 100;
8console.log(o.nested.b);
9// → 100        spread is SHALLOW: nested is the same object
10
11console.log({ a: 1 } === { a: 1 });   // → false   different references
12console.log([1] == "1");              // → true    [1] → "1"
13
14const keys = {};
15keys[{ id: 1 }] = "first";
16keys[{ id: 2 }] = "second";
17console.log(Object.keys(keys), keys[{ id: 3 }]);
18// → [ '[object Object]' ] second
19// → all objects stringify to the same key and overwrite each other
20
21const frozen = Object.freeze({ x: 1, deep: { y: 2 } });
22frozen.x = 9;
23frozen.deep.y = 9;
24console.log(frozen.x, frozen.deep.y);
25// → 1 9        freeze is shallow too

The shallow-freeze result surprises almost everyone, and it is the same rule as the shallow spread one line above. If the follow-up asks how to fix it, the answer is a recursive freeze or a deep clone with `structuredClone`, and noting that both are shallow for the same reason ties the whole section together.

Array traps

Arrays are objects with a length, which explains most of what looks strange about them: holes are missing properties rather than undefined values, `delete` does not shorten anything, and the default sort compares strings.

Sort, holes, delete and lengthjavascript
1console.log([1, 10, 9, 2].sort());
2// → [ 1, 10, 2, 9 ]        default sort compares strings
3
4console.log(new Array(3).map((_, i) => i));
5// → [ <3 empty items> ]    map SKIPS holes; the callback never ran
6
7console.log(Array.from({ length: 3 }, (_, i) => i));
8// → [ 0, 1, 2 ]
9
10const a = [1, 2, 3];
11delete a[1];
12console.log(a, a.length);
13// → [ 1, <1 empty item>, 3 ] 3     delete leaves a hole
14
15const b = [1, 2, 3];
16b.length = 1;
17console.log(b);
18// → [ 1 ]                  assigning length truncates
19
20console.log([].every((x) => x > 5), [].some((x) => x > 5));
21// → true false             vacuous truth on an empty array
22
23console.log([1, 2, 3].map(parseInt));
24// → [ 1, NaN, NaN ]
25// map passes (value, index), so it calls parseInt("2", 1), radix 1
26// is invalid - and parseInt("3", 2), where 3 is not a binary digit

`[1, 2, 3].map(parseInt)` is the single most-asked array output question and the one people most often get wrong for the wrong reason. It is not about `parseInt` being strange, it is that `map` passes three arguments and `parseInt` accepts two, so the index arrives as the radix. `map(Number)` gives the expected result because `Number` takes only one argument.

Prototypes, classes and error handling

A smaller family, but it appears at product companies. The two rules that generate most of it: properties are looked up along the prototype chain, and `finally` runs even when the `try` block returns.

Chain lookups and the finally overridejavascript
1function A() {}
2A.prototype.greet = function () { return "hi"; };
3const a = new A();
4console.log(a.greet(), Object.hasOwn(a, "greet"));
5// → hi false               found on the prototype, not the instance
6
7a.greet = function () { return "own"; };
8console.log(a.greet(), new A().greet());
9// → own hi                 the own property shadows, it does not replace
10
11function tricky() {
12  try {
13    return "from try";
14  } finally {
15    console.log("finally runs");
16  }
17}
18console.log(tricky());
19// → finally runs
20// → from try                finally executes BEFORE the value is returned
21
22function overridden() {
23  try { return "try"; } finally { return "finally"; }
24}
25console.log(overridden());
26// → finally                 a return in finally replaces the try's value
27
28async function boom() { throw new Error("x"); }
29boom().catch((e) => console.log("caught", e.message));
30// → caught x                a throw in an async fn becomes a rejection

The shadowing example is worth dwelling on, because it explains something people find arbitrary. Assigning to `a.greet` creates an own property on the instance, which is found first during lookup and therefore hides the prototype's version, but it does not replace it, so a new instance still gets the original. That is the whole prototype chain in one snippet, and it is the reason methods defined in a class are shared while instance fields are not.

The `finally` pair is the other one to have ready. `finally` always runs before the function actually hands back a value, so the log appears before the returned string, and a `return` inside `finally` replaces whatever `try` was about to return, silently discarding it. It is legal and it is always a bug, which is why linters flag it and interviewers like it.

Numbers, strings and a few one-liners

A last group that does not fit the six families cleanly but shows up often enough to prepare. Most of it comes from two facts: every number is a 64-bit float, and strings are indexed by UTF-16 code unit rather than by character.

Floats, string length and a couple of classicsjavascript
1console.log(0.1 + 0.2 === 0.3);   // → false     64-bit float rounding
2console.log(0.1 + 0.2);           // → 0.30000000000000004
3console.log(9007199254740992 + 1); // → 9007199254740992   past MAX_SAFE_INTEGER
4
5console.log("नमस्ते".length);        // → 6      code units, not characters
6console.log([..."👍"].length);      // → 1      spread iterates by code point
7console.log("👍".length);           // → 2      one emoji, two code units
8
9console.log("b" + "a" + +"a" + "a");
10// → baNaNa     +"a" is NaN, which stringifies to "NaN"
11
12console.log(typeof typeof 1);     // → string   typeof always returns a string
13
14console.log(Math.max());          // → -Infinity   the identity for max
15console.log(Math.min());          // → Infinity
16
17console.log([10, 1, 3].sort() + "");
18// → 1,10,3     sorted as strings, then joined by the + coercion

The string length results are the genuinely useful ones here, because they affect real products rather than just interviews. A character counter built on `.length` miscounts every emoji and most Indic script, so a 280-character limit rejects text a user can see is shorter. Spreading the string or using `Intl.Segmenter` counts what a reader would call characters, and mentioning that turns a trivia answer into a product observation, which is a considerably better note to end an output round on.

How to practise these properly

Cover the output before you read it, write your prediction down, then run the snippet. The writing matters - an answer you thought about is easy to convince yourself you knew once you have seen the right one, and that self-deception is why reading lists of these does almost nothing. Ten snippets predicted in writing are worth more than a hundred read.

Run them somewhere the output is honest. A browser console is fine for most of these, but be aware that it evaluates each entry as an expression rather than a statement, which is exactly why `{} + []` behaves differently there than at the top of a script. Node, or a snippet inside a real file, avoids that distinction. And when a result surprises you, change one line and predict again, flipping `var` to `let`, or moving a `console.log` above an `await`, is how you check that you learnt the rule rather than the sentence.

When you are wrong, do not note the answer. Identify which of the six families you misread, then go and re-read that model properly, the companion page on tricky JavaScript questions linked below explains each one. A model handles snippets you have never seen; a memorised answer handles exactly one snippet and collapses the moment the interviewer changes a line, which they will, because that is how they check.

Frequently asked questions

Why do interviewers ask output-based questions?
They take fifteen seconds to pose and are almost impossible to bluff. Predicting output correctly requires an actual model of hoisting, closures, the event loop or coercion, and the follow-up, why?, immediately separates understanding from a memorised answer.
Why does a var loop with setTimeout print 3 3 3?
var creates one binding for the entire function, so all three callbacks close over the same variable and read it after the loop has finished, when it holds 3. let creates a fresh binding each iteration, giving 0 1 2. Pushing arrow functions into an array shows the same result with no timer at all.
Why does a promise callback run before setTimeout with zero delay?
Promise callbacks are microtasks and timers are macrotasks. The event loop drains the entire microtask queue after the current synchronous work finishes, before taking a single macrotask, and again between every pair of macrotasks. So .then always wins.
Does an async function start running asynchronously?
No. The body executes synchronously up to the first await; only the continuation after it becomes a microtask. That is why a console.log before the first await prints during the synchronous phase, alongside the code that called the function.
Why is [] == ![] true in JavaScript?
![] is false because an empty array is truthy. Then [] == false coerces both sides to numbers: false becomes 0, and [] becomes "" which becomes 0. So the comparison ends as 0 == 0. It is the clearest single demonstration of why == should be avoided.
What does 1 + "2" - 1 evaluate to?
11. The + operator prefers string concatenation when either operand is a string, producing "12", and then the - operator forces both sides to numbers, giving 12 - 1. Every arithmetic operator except + coerces to number, which is the rule the whole family of questions rests on.
Why does [1, 2, 3].map(parseInt) return [1, NaN, NaN]?
map calls its callback with the value, the index and the array, and parseInt takes a string and a radix, so it runs parseInt("2", 1) and parseInt("3", 2). Radix 1 is invalid and 3 is not a valid binary digit, so both give NaN. map(Number) works because Number takes one argument.
Why does new Array(3).map() not run the callback?
new Array(3) creates three holes rather than three undefined values, and map skips holes entirely. Use Array.from({ length: 3 }, (_, i) => i), which never creates holes, or call fill() before map.
Why is this undefined inside a function declared within a method?
Because a plain function call gets the default binding regardless of where the function was written. Only the call site matters, and there is nothing before the dot. Making the inner function an arrow fixes it, because arrows take this from the enclosing scope.
What happens when finally contains a return?
It overrides whatever the try or catch block was returning. finally always runs before the function actually returns, so a value returned from it replaces the earlier one, which is why returning from finally is considered a bug even though it is legal.
Is Object.freeze deep?
No, it is shallow. Top-level properties become read-only, but nested objects remain fully mutable, exactly like the spread operator. Freeze recursively if you need the whole tree, and note that in non-strict mode a rejected write fails silently rather than throwing.
How do I get better at output questions?
Cover the answer, write your prediction down, then run the code. When you are wrong, work out which model you misread, hoisting, closures, the event loop, this, coercion or references, and re-learn that model rather than memorising the answer. A model generalises to snippets you have never seen.

This is 1 of 80+ questions in the JavaScript Interview Kit

Get every question with detailed answers, follow-ups and real code, plus coding challenges and a last-minute revision sheet. One-time payment, instant access.

⚡ Get the JavaScript Interview Kit → ₹299

Full kit

JavaScript Interview Kit · ₹299

Get it →