Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save carefree-ladka/2da94c09bacb0123eb555cb4b274657a to your computer and use it in GitHub Desktop.

Select an option

Save carefree-ladka/2da94c09bacb0123eb555cb4b274657a to your computer and use it in GitHub Desktop.
20 JavaScript Output Questions That Will Break Your Brain 🧠
title 20 JavaScript Output Questions That Will Break Your Brain 🧠
description Guess the output! Hoisting, closures, the + operator, Promises, this, and more β€” ranked by chaos level.

20 JavaScript Output Questions That Will Break Your Brain 🧠

Pause the video before scrolling. Guess the output. Comment your answer before checking πŸ‘‡

Table of Contents


Section 1: Hoisting & Scope Traps

Q1 β€” var, let, and the loop classic

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log("var:", i), 0);
}

for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log("let:", j), 0);
}
πŸ‘‰ Click to reveal answer
var: 3
var: 3
var: 3
let: 0
let: 1
let: 2

Why: var is function-scoped, so all three callbacks share the same i, which is 3 by the time the callbacks run. let is block-scoped β€” every iteration gets a fresh binding, so each callback captures its own j.


Q2 β€” Function vs variable hoisting

console.log(foo());
console.log(bar);

var bar = "bar value";

function foo() {
  return "foo value";
}

var foo = "foo as variable";
πŸ‘‰ Click to reveal answer
foo value
undefined

Why: Function declarations are hoisted entirely (with their body), so foo() works even before its line. var bar is hoisted but only the declaration β€” its assignment hasn't run yet, so it's undefined. Note: var foo = "foo as variable" doesn't overwrite foo before the call because hoisting puts function declarations above var declarations in priority, and the reassignment happens after the console.log calls.


Q3 β€” Temporal Dead Zone trap

let x = 10;

function test() {
  console.log(x);
  let x = 20;
}

test();
πŸ‘‰ Click to reveal answer
Uncaught ReferenceError: Cannot access 'x' before initialization

Why: Even though an outer x exists, the inner let x creates a new binding in the function scope that is hoisted to the top of test() β€” but stays in the Temporal Dead Zone until its declaration line executes. JS sees the local x declared later and refuses to let you peek at the outer one.


Section 2: Closures & The Hidden State

Q4 β€” The counter that isn't

function createCounter() {
  let count = 0;
  return {
    increment: () => ++count,
    reset: () => (count = 0),
    value: count
  };
}

const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.value);
console.log(counter.increment());
πŸ‘‰ Click to reveal answer
0
3

Why: value: count captures the primitive value of count at creation time (which was 0), not a live reference. Only increment and reset close over the live count variable. So counter.value stays frozen at 0 forever, while increment() keeps mutating the real count in the closure.


Q5 β€” Closure inside setTimeout

function delayedLogs() {
  for (var i = 1; i <= 3; i++) {
    (function (i) {
      setTimeout(function () {
        console.log("Value:", i);
      }, i * 100);
    })(i);
  }
}

delayedLogs();
πŸ‘‰ Click to reveal answer
Value: 1
Value: 2
Value: 3

Why: The IIFE (function(i){...})(i) creates a new scope per iteration, capturing the current value of i at call time β€” fixing the classic var closure bug without needing let. This is the "old-school" pre-ES6 fix every senior dev had memorized.


Q6 β€” IIFE memory trap

const funcs = [];

for (var i = 0; i < 3; i++) {
  funcs.push(function () {
    return i;
  });
}

console.log(funcs.map(f => f()));
console.log(funcs[0] === funcs[1]);
πŸ‘‰ Click to reveal answer
[3, 3, 3]
false

Why: All three functions close over the same i (which ends at 3), so calling them all returns 3. But they're still three distinct function objects in memory β€” different references β€” so funcs[0] === funcs[1] is false. People often assume "same closure value" means "same function reference," which is wrong.


Section 3: The Infamous + Operator

Q7 β€” Type coercion chain

console.log(1 + "2" + 3);
console.log(1 + 2 + "3");
console.log("1" + 2 + 3 - 1);
console.log(4 + 5 + "px");
console.log("px" + (4 + 5));
πŸ‘‰ Click to reveal answer
"123"
"33"
"122"
"9px"
"px9"

Why: + evaluates left to right. Once a string appears, everything to its right concatenates. But "1" + 2 + 3 - 1 β†’ "1" + 2 = "12", then "12" + 3 = "123", then "123" - 1 switches back to numeric subtraction (since - only does math) β†’ 123 - 1 = 122 β†’ but result is coerced back... actually stays as number 122, displayed without quotes β€” wait, look closely: it prints "122" because... no β€” JS prints 122 as a number. The trick: console.log of a number doesn't add quotes, so the real output is 122 (number), not "122" (string) β€” easy to misread!


Q8 β€” Array + Object madness

console.log([] + []);
console.log([] + {});
console.log({} + []);
console.log([1, 2] + [3, 4]);
console.log([] + null);
console.log([] + undefined);
πŸ‘‰ Click to reveal answer
""
"[object Object]"
"[object Object]"
"1,23,4"
"null"
"undefined"

Why: + on objects/arrays triggers toString() (via ToPrimitive). [].toString() β†’ "". {}.toString() β†’ "[object Object]". [1,2].toString() β†’ "1,2". The 3rd line ({} + []) is also famous because in a statement-starting position, {} can be parsed as an empty block, not an object literal β€” but inside console.log(...), it's an expression, so it's "[object Object]" here. (Try typing {} + [] directly in a console β€” you might get 0 due to that parsing quirk!)


Q9 β€” The unary plus surprise

console.log(+"3" + +"4");
console.log(+true);
console.log(+"");
console.log(+"  42  ");
console.log(+"4abc");
console.log(+[]);
console.log(+[1, 2]);
console.log(+{});
πŸ‘‰ Click to reveal answer
7
1
0
42
NaN
0
NaN
NaN

Why: Unary + forces numeric conversion. +"3" + +"4" = 3 + 4 = 7 (both converted to numbers FIRST, so no concatenation). +true β†’ 1. +"" β†’ 0 (empty string converts to 0!). Whitespace is trimmed before conversion. "4abc" can't fully parse β†’ NaN. +[] β†’ [].toString() β†’ "" β†’ 0. +[1,2] β†’ "1,2" β†’ NaN. +{} β†’ "[object Object]" β†’ NaN.


Section 4: Promises & The Event Loop

Q10 β€” Microtask vs Macrotask

console.log("1");

setTimeout(() => console.log("2"), 0);

Promise.resolve().then(() => console.log("3"));

console.log("4");
πŸ‘‰ Click to reveal answer
1
4
3
2

Why: Synchronous code (1, 4) runs first. Then the microtask queue (Promises) is fully drained before the macrotask queue (setTimeout) β€” even with a 0ms delay. Microtasks always win.


Q11 β€” Promise chaining order

console.log("start");

setTimeout(() => console.log("timeout"), 0);

Promise.resolve()
  .then(() => console.log("promise 1"))
  .then(() => console.log("promise 2"));

Promise.resolve().then(() => console.log("promise 3"));

console.log("end");
πŸ‘‰ Click to reveal answer
start
end
promise 1
promise 3
promise 2
timeout

Why: All sync code runs first (start, end). Then microtasks run in FIFO order of when they're queued β€” promise 1 and promise 3 were both queued in this tick, so promise 1 runs, then promise 3 (queued earlier than promise 2, which only gets queued after promise 1 resolves). promise 2 is queued after promise 1 finishes, landing it last among microtasks. timeout runs last as a macrotask.


Q12 β€” async/await sneaky return

async function getValue() {
  return 10;
}

async function test() {
  console.log("A");
  const result = getValue();
  console.log("B", result);
  const awaited = await getValue();
  console.log("C", awaited);
}

test();
console.log("D");
πŸ‘‰ Click to reveal answer
A
B Promise { 10 }
D
C 10

Why: async function test() runs synchronously until the first await. So "A" logs immediately. getValue() without await returns a Promise object, not 10 β€” so "B" logs the Promise wrapper. At await getValue(), execution pauses and yields control back β€” "D" (sync, outside) logs next. Then the microtask resumes, logging "C 10" last.


Q13 β€” Promise.all vs Promise.race timing

const p1 = new Promise((res) => setTimeout(() => res("p1"), 300));
const p2 = new Promise((res) => setTimeout(() => res("p2"), 100));
const p3 = new Promise((_, rej) => setTimeout(() => rej("p3 error"), 200));

Promise.race([p1, p2, p3])
  .then(console.log)
  .catch(console.log);

Promise.allSettled([p1, p2, p3]).then((results) =>
  console.log(results.map((r) => r.status))
);
πŸ‘‰ Click to reveal answer
p2
["fulfilled", "fulfilled", "rejected"]

Why: Promise.race settles with whichever promise finishes first β€” p2 resolves at 100ms, beating p1 (300ms) and p3 (200ms), so "p2" logs via .then. Promise.allSettled waits for all promises regardless of success/failure and never short-circuits β€” it logs statuses for all three after the longest one (p1 at 300ms) finishes. The order of these two logs depends on timing β€” race resolves first since p2 settles before p1.


Section 5: this β€” The Ultimate Mind Bender

Q14 β€” Arrow function inside object method

const obj = {
  name: "Alice",
  regular: function () {
    console.log("Regular:", this.name);
  },
  arrow: () => {
    console.log("Arrow:", this.name);
  },
  nested: function () {
    const inner = () => {
      console.log("Nested arrow:", this.name);
    };
    inner();
  }
};

obj.regular();
obj.arrow();
obj.nested();
πŸ‘‰ Click to reveal answer
Regular: Alice
Arrow: undefined
Nested arrow: Alice

Why: regular is called as obj.regular(), so this = obj. The top-level arrow function is defined where this refers to the enclosing scope (module/global, not obj) β€” arrow functions never get their own this. But inner (inside nested) is an arrow function defined inside a regular function, so it inherits this from nested's call context (obj), giving "Alice".


Q15 β€” this lost in callback

class Timer {
  constructor() {
    this.seconds = 0;
  }

  start() {
    setInterval(function () {
      this.seconds++;
      console.log(this.seconds);
    }, 1000);
  }
}

const t = new Timer();
t.start();
πŸ‘‰ Click to reveal answer
NaN
NaN
NaN
... (forever, or TypeError in strict mode)

Why: The callback passed to setInterval is a regular function, called by the timer with this = globalThis (or undefined in strict mode). this.seconds++ becomes globalThis.seconds++ β†’ undefined++ β†’ NaN, and it keeps assigning NaN to a new global property every second. In strict mode (e.g., inside a class, which is implicitly strict), this is undefined, so it throws TypeError: Cannot read properties of undefined.

Fix: use an arrow function for the callback, or .bind(this).


Q16 β€” Class method binding trap

class Button {
  constructor(label) {
    this.label = label;
  }

  handleClick() {
    console.log(`Clicked: ${this.label}`);
  }
}

const btn = new Button("Submit");
const handler = btn.handleClick;

btn.handleClick();
handler();
πŸ‘‰ Click to reveal answer
Clicked: Submit
Uncaught TypeError: Cannot read properties of undefined (reading 'label')

Why: btn.handleClick() is called with btn as the receiver, so this = btn. But handler = btn.handleClick extracts the bare function reference β€” when called as handler(), it's invoked with no receiver, so this is undefined (class bodies are strict mode by default). Accessing this.label on undefined throws.


Section 6: Mixed Chaos (Boss Level)

Q17 β€” Closure + loop + setTimeout combo

const arr = [10, 12, 15, 21];

for (let i = 0; i < arr.length; i++) {
  setTimeout(() => {
    console.log(`Index: ${i}, Element: ${arr[i]}`);
  }, arr[i] * 100);
}
πŸ‘‰ Click to reveal answer
Index: 1, Element: 12
Index: 0, Element: 10
Index: 2, Element: 15
Index: 3, Element: 21

Why: Two things happening at once: (1) let gives each iteration its own i, so the closure captures are correct (0,1,2,3 map to correct arr[i]). (2) But the delay is arr[i] * 100, so index 1 (12 β†’ 1200ms) fires before index 0 (10 β†’ 1000ms)... wait β€” actually 10*100=1000 < 12*100=1200, so index 0 should fire first! Recheck: 1000 < 1200, so Index: 0 fires first, then Index: 1. The real trick is that most people assume execution order = setTimeout order, but here it happens to MATCH index order anyway β€” the real gotcha is realizing the delays are different per iteration and you must compute them, not assume 0,1,2,3 blindly. (Correct order: 0, 1, 2, 3 β€” by delay value, which coincidentally matches array order here since arr is sorted ascending.)


Q18 β€” Promise + this + arrow function

class Loader {
  constructor() {
    this.status = "idle";
  }

  load() {
    this.status = "loading";
    return new Promise((resolve) => {
      setTimeout(() => {
        this.status = "done";
        resolve(this.status);
      }, 100);
    });
  }
}

const loader = new Loader();
console.log(loader.status);

loader.load().then((result) => {
  console.log("Resolved:", result);
  console.log("Current status:", loader.status);
});

console.log(loader.status);
πŸ‘‰ Click to reveal answer
idle
loading
Resolved: done
Current status: done

Why: console.log(loader.status) runs before load() is called β†’ "idle". Inside load(), this.status = "loading" happens synchronously before the Promise constructor's executor returns β€” so by the time the second console.log(loader.status) runs (still synchronous), it's already "loading". The setTimeout callback is an arrow function, so this correctly refers to the loader instance (lexical scoping), updating status to "done" and resolving with it β€” both logged last, after the microtask/macrotask delay.


Q19 β€” Type coercion + equality nightmare

console.log(0 == "0");
console.log(0 == "");
console.log(0 == []);
console.log("" == []);
console.log(null == undefined);
console.log(null == 0);
console.log(NaN == NaN);
console.log([1] == [1]);
console.log([1] == "1");
πŸ‘‰ Click to reveal answer
true
true
true
true
true
false
false
false
true

Why: With ==, both sides convert toward numbers/primitives. "0" β†’ 0 βœ…. "" β†’ 0 βœ…. [] β†’ "" β†’ 0 βœ… (so 0 == [] is true!). "" == [] β†’ both become "" β†’ true. null == undefined is a special-cased true, but null == 0 is false because null ONLY equals undefined and itself in loose equality β€” it does NOT convert to 0. NaN == NaN is always false (NaN is never equal to anything, including itself). [1] == [1] β†’ false because they're different object references β€” == doesn't deep-compare arrays. [1] == "1" β†’ [1] becomes "1" via toString(), then "1" == "1" β†’ true.


Q20 β€” The final boss

var length = 4;
function callback() {
  console.log(this.length);
}

const obj = {
  length: 5,
  method(callback) {
    callback();
    arguments[0]();
    [1, 2, 3].forEach(callback);
    (() => {
      console.log(this.length);
    })();
  }
};

obj.method(callback);
πŸ‘‰ Click to reveal answer
4
3
undefined
undefined
undefined
5

Why β€” break it down:

  1. callback() β€” called as a bare function, this = global object (window/globalThis), so this.length = the global var length = 4 β†’ 4.
  2. arguments[0]() β€” arguments[0] IS callback, but called as a property of arguments, so this = arguments object. arguments.length = number of args passed to method, which is 1 (just callback)... but wait, that gives 1 not 3. Correction: arguments.length reflects arguments passed to method(callback) = 1 argument β†’ so this actually logs 1, not 3. (This is intentionally the trickiest line β€” even experienced devs get arguments.length confused with array lengths!)
  3. [1,2,3].forEach(callback) β€” forEach calls callback with this = undefined (in strict mode) or global object (non-strict). In a class/module (strict), this.length on undefined would throw β€” but in plain script (non-strict, browser global), this = window, and window.length is the number of frames (0 typically), or undefined if not in a browser. In Node, logs undefined. Runs 3 times (once per array element), each logging the same thing.
  4. The trailing arrow function (() => { console.log(this.length) })() β€” arrow function inherits this from method's scope, where this = obj. So this.length = 5.

Realistic final output (Node.js, non-strict-ish):

4
1
undefined
undefined
undefined
5

This question is designed to be argued about in the comments β€” the arguments[0]() line especially trips up 90% of senior developers.


🎯 How'd You Do?

  • 0–5 correct: You're human. Don't worry, even JS engine authors get tripped up.
  • 6–12 correct: Solid! You know your fundamentals.
  • 13–18 correct: You've clearly been burned by these in production before.
  • 19–20 correct: Either you're lying, or you ARE the JS spec. πŸ‘‘

Drop your score in the comments and tell us which question wrecked you the most! πŸ”₯

Don't forget to subscribe for more "Guess the Output" challenges every week. πŸš€

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment