Output-Based Questions

Difficulty: Advanced

Output-based questions are a staple of JavaScript interviews. The interviewer shows you a code snippet and asks you to predict exactly what will be logged to the console and in what order. These questions test your deep understanding of JavaScript's execution model, including the event loop, hoisting, closures, type coercion, 'this' binding, and scope rules. Success requires methodical mental execution of code rather than guessing.

The most important concept for output questions is the event loop and task ordering. JavaScript is single-threaded and uses an event loop with a call stack, a microtask queue (for Promises, queueMicrotask, MutationObserver), and a macrotask queue (for setTimeout, setInterval, I/O). The execution order is: (1) run all synchronous code on the call stack, (2) drain all microtasks, (3) run one macrotask, (4) drain all microtasks again, (5) repeat. This means Promise.then() callbacks always execute before setTimeout callbacks, even if the setTimeout has a 0ms delay.

var vs let in loops with setTimeout is a classic question. When you use var in a for loop with setTimeout, the variable is function-scoped - all iterations share the same variable. By the time the setTimeout callbacks run, the loop has completed and the variable holds its final value. With let, each iteration creates a new block-scoped variable, so each callback captures its own copy. This is perhaps the single most asked output question in interviews.

Type coercion produces some of JavaScript's most surprising behaviors. The + operator triggers string concatenation if either operand is a string, but numeric addition otherwise. Empty arrays coerce to empty strings, objects coerce to '[object Object]', null coerces to 0 in numeric context, and undefined coerces to NaN. The == operator performs type coercion while === does not. Understanding the abstract equality comparison algorithm is key to predicting == results.

Hoisting affects output in subtle ways. var declarations are hoisted to the top of their function scope but their assignments are not - so accessing a var before its assignment returns undefined (not a ReferenceError). Function declarations are fully hoisted (both name and body), while function expressions (including arrow functions assigned to variables) are only hoisted as variable declarations. let and const are hoisted but placed in the Temporal Dead Zone - accessing them before their declaration throws a ReferenceError.

'this' keyword behavior depends entirely on how a function is called, not where it is defined. In a regular function call, 'this' is the global object (or undefined in strict mode). In a method call, 'this' is the object before the dot. In a constructor, 'this' is the new instance. Arrow functions inherit 'this' from their enclosing scope. bind, call, and apply explicitly set 'this'. Knowing these rules is essential for predicting output in any code that uses 'this'.

Code examples

Event loop ordering: sync, Promise, setTimeout

console.log('1: Start');

setTimeout(() => {
  console.log('2: setTimeout 1');
}, 0);

Promise.resolve().then(() => {
  console.log('3: Promise 1');
}).then(() => {
  console.log('4: Promise 2');
});

setTimeout(() => {
  console.log('5: setTimeout 2');
}, 0);

Promise.resolve().then(() => {
  console.log('6: Promise 3');
});

console.log('7: End');

Order: (1) All synchronous code runs first: 'Start' and 'End'. (2) Microtasks (Promises) drain completely: Promise 1, Promise 3, then Promise 2 (chained from Promise 1). (3) Macrotasks (setTimeouts) run one by one. Note: setTimeout order may vary between engines but Promises always precede setTimeouts.

var vs let in loops with setTimeout

// With var - shares one variable across all iterations
console.log('=== var ===');
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log('var:', i), 0);
}

// With let - each iteration gets its own variable
console.log('=== let ===');
for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log('let:', j), 0);
}

// Fix with var: IIFE creates a new scope
console.log('=== var + IIFE ===');
for (var k = 0; k < 3; k++) {
  (function(k) {
    setTimeout(() => console.log('iife:', k), 0);
  })(k);
}

// What value does i have after the loop?
console.log('i after loop:', i); // var is function-scoped

With var, all three callbacks share the same 'i' which is 3 after the loop finishes. With let, each iteration creates a new 'j' in its own block scope, so each callback captures the correct value. The IIFE fix creates a new function scope for each iteration, capturing the current value of k.

Type coercion surprises

// String concatenation vs addition
console.log(1 + '2');         // string concatenation
console.log('3' - 1);         // numeric subtraction
console.log('3' + 1);         // string concatenation
console.log(+'3');             // unary + converts to number

// Array coercion
console.log([] + []);          // '' + '' = ''
console.log([] + {});          // '' + '[object Object]'
console.log({} + []);          // '[object Object]' + ''
console.log([] == false);      // [] -> '' -> 0, false -> 0
console.log(!![]);             // [] is truthy!

// null and undefined
console.log(null == undefined);  // true (special rule)
console.log(null === undefined); // false (different types)
console.log(null + 1);          // null -> 0
console.log(undefined + 1);     // undefined -> NaN

// typeof oddities
console.log(typeof null);       // 'object' (historical bug)
console.log(typeof undefined);  // 'undefined'
console.log(typeof NaN);        // 'number'
console.log(NaN === NaN);       // false!

The + operator prefers string concatenation if either operand is a string. Arrays coerce to strings via .toString() (empty array becomes ''). null coerces to 0 in numeric context but undefined coerces to NaN. typeof null is 'object' due to a historical bug. NaN is not equal to itself - use Number.isNaN() instead.

Hoisting and Temporal Dead Zone

// var hoisting
console.log(a); // hoisted declaration, not assignment
var a = 5;
console.log(a);

// Function declaration vs expression
console.log(foo()); // works - fully hoisted
// console.log(bar()); // TypeError: bar is not a function

function foo() { return 'foo declaration'; }
var bar = function() { return 'bar expression'; };

// let/const TDZ
try {
  console.log(x);
  let x = 10;
} catch (e) {
  console.log(e.constructor.name);
}

// Tricky: function in block
var c = 1;
function outer() {
  console.log(c); // undefined - var c below is hoisted
  var c = 2;
  console.log(c);
}
outer();

// Variable shadowing
var d = 'global';
function inner() {
  console.log(d); // undefined - local var d hoisted
  var d = 'local';
  console.log(d);
}
inner();

var declarations are hoisted but assigned undefined until the assignment line executes. Function declarations are fully hoisted. let/const throw ReferenceError in the Temporal Dead Zone (between hoisting and declaration). Local var declarations shadow outer variables - the hoisted local var makes the outer variable inaccessible.

Closure and 'this' output patterns

// Closure captures variables by reference
function createFunctions() {
  const fns = [];
  for (let i = 0; i < 3; i++) {
    fns.push(() => i * i);
  }
  return fns;
}

const fns = createFunctions();
console.log(fns[0](), fns[1](), fns[2]());

// 'this' in different contexts
const obj = {
  name: 'Alice',
  regular: function() {
    return this.name;
  },
  arrow: () => {
    return this.name; // 'this' is from outer scope (global/undefined)
  },
  nested: function() {
    const inner = () => this.name; // arrow inherits 'this' from nested
    return inner();
  }
};

console.log(obj.regular());
console.log(obj.arrow());
console.log(obj.nested());

// Method extraction loses 'this'
const { regular } = obj;
try {
  console.log(regular());
} catch (e) {
  console.log('Lost this: undefined in strict mode');
}

// Tricky setTimeout with 'this'
const timer = {
  name: 'Timer',
  start: function() {
    setTimeout(function() {
      console.log('setTimeout regular:', this.name); // undefined
    }, 0);
    setTimeout(() => {
      console.log('setTimeout arrow:', this.name); // 'Timer'
    }, 0);
  }
};
timer.start();

With let, each closure captures its own 'i'. Arrow functions inherit 'this' from their enclosing scope - obj.arrow's enclosing scope is the module/global, not the object. obj.nested's arrow inherits 'this' from the nested function. In setTimeout, regular functions lose 'this' but arrow functions preserve it from the enclosing method.

Key points

Concepts covered

Event Loop, Hoisting, Closures, Type Coercion, this Keyword, Scope, Promise Ordering, setTimeout