Episode 1 — Fundamentals / 1.19 — Conditionals and Loops

Interview Questions: Conditionals and Loops

Model answers for if/else, switch, for/while loops, break/continue, closures in loops, and algorithm patterns.

How to use this material (instructions)

  1. Read lessons in order -- README.md, then 1.19.a through 1.19.f.
  2. Practice out loud -- definition, example, pitfall.
  3. Pair with exercises -- 1.19-Exercise-Questions.md.
  4. Quick review -- 1.19-Quick-Revision.md.

Beginner (Q1--Q6)

Q1. What are truthy and falsy values in JavaScript? How do they relate to if statements?

Why interviewers ask: Confirms you understand implicit type coercion in conditionals -- a source of many subtle bugs.

Model answer:

JavaScript's if statement coerces its condition to a boolean. Falsy values are the six values that coerce to false: false, 0 (and -0, 0n), "" (empty string), null, undefined, and NaN. Everything else is truthy, including empty objects {}, empty arrays [], the string "0", and the string "false". This means if ([]) enters the block (the array is truthy even though it is empty), while if ("") does not. Best practice: use explicit comparisons (=== null, .length === 0) when the intent needs to be clear.


Q2. Explain the difference between == and === in an if condition.

Why interviewers ask: Type coercion bugs are a top JavaScript pain point.

Model answer:

== (abstract equality) performs type coercion before comparing: "5" == 5 is true because the string is converted to a number. === (strict equality) checks type and value with no coercion: "5" === 5 is false. In if conditions, always prefer === to avoid unexpected matches. The only common exception is value == null, which conveniently checks for both null and undefined in one comparison.


Q3. What is the ternary operator? When should and should not you use it?

Why interviewers ask: Tests if you know concise syntax without sacrificing readability.

Model answer:

The ternary operator condition ? exprIfTrue : exprIfFalse is a single-expression alternative to if/else. It is ideal for inline assignments (const status = age >= 18 ? "adult" : "minor") and template literals. Avoid nesting ternaries or using them for side effects (function calls with no return value), because readability drops sharply. When logic exceeds one level, use if/else or extract a function.


Q4. What happens if you omit break in a switch case?

Why interviewers ask: Fall-through is the most common switch bug and also a deliberate pattern.

Model answer:

Without break (or return), execution falls through to the next case block regardless of whether it matches. This is intentional in the language spec but usually a bug. The classic legitimate use is grouping cases that share the same handler (e.g., case "Saturday": case "Sunday": ... break;). To prevent accidental fall-through, enable ESLint's no-fallthrough rule and always terminate each case with break, return, or throw.


Q5. What is the difference between for...of and for...in?

Why interviewers ask: Mixing them up on arrays is a common mistake.

Model answer:

for...of iterates over values of any iterable (arrays, strings, Maps, Sets). for...in iterates over enumerable property names (keys) of an object, returned as strings. For arrays, for...in gives string indices ("0", "1") and may include non-index enumerable properties (e.g., from prototype extensions or custom properties added to the array). Rule: Use for...of for arrays and iterables, for...in for plain objects. For objects, Object.keys() or Object.entries() with for...of is often even cleaner.


Q6. Explain while vs do...while. When would you choose one over the other?

Why interviewers ask: Tests understanding of loop semantics and practical judgment.

Model answer:

while (condition) { body } checks the condition before each iteration -- the body may never run (zero or more executions). do { body } while (condition); runs the body first, then checks the condition -- guaranteeing at least one execution. Choose do...while when the first iteration must always happen, such as prompting for user input (you need to ask at least once before you can validate). In most other cases, while is the default choice.


Intermediate (Q7--Q12)

Q7. Explain the let vs var closure trap in for loops.

Why interviewers ask: One of the most asked JavaScript questions -- tests closure + scope knowledge.

Model answer:

With var, the loop variable is function-scoped -- all iterations share the same variable. If you create closures inside the loop (e.g., setTimeout), they all capture the same reference and see the final value after the loop ends.

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

With let, each iteration gets a new block-scoped binding, so each closure captures its own copy:

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// Output: 0, 1, 2

Before ES6, the fix was an IIFE: (function(j) { setTimeout(() => console.log(j), 0); })(i);. Modern code should always use let.


Q8. How do break, continue, and return differ inside a loop that is inside a function?

Why interviewers ask: Confusion here causes logic bugs.

Model answer:

  • break exits the innermost loop (or switch). Code after the loop runs normally.
  • continue skips the rest of the current iteration and proceeds to the next iteration of the innermost loop.
  • return exits the entire function, not just the loop. No code after the return statement executes.

In nested loops, break and continue only affect the innermost loop unless a label is used. return always exits the function regardless of nesting depth.


Q9. What are labeled statements? When are they appropriate?

Why interviewers ask: Tests depth of language knowledge and judgment about code clarity.

Model answer:

A label is an identifier followed by a colon, placed before a loop: outer: for (...). You can then write break outer or continue outer to target that specific loop from within a nested loop. This is useful for breaking out of a double loop (e.g., searching a 2D grid) without a flag variable. However, labels are rarely needed in practice -- extracting the nested loop into a separate function and using return is usually cleaner and more testable. Use labels only when extraction would be awkward or the code is performance-critical.


Q10. What is a guard clause? Why do experienced developers prefer them?

Why interviewers ask: Code quality and maintainability awareness.

Model answer:

A guard clause is an early if (...) return (or throw) at the top of a function that rejects invalid inputs immediately, avoiding nested if-else structures. Benefits: (1) each validation is a clear, single-purpose line; (2) the "happy path" code runs at the top indentation level, making it easy to scan; (3) adding new validations is a one-line change. Example:

function createUser(name, email) {
  if (!name) throw new Error("Name required");
  if (!email.includes("@")) throw new Error("Invalid email");
  // happy path at top level
  return { name, email, createdAt: Date.now() };
}

Q11. Can you use switch with expressions (not just literals) in case clauses?

Why interviewers ask: Tests deeper understanding of how switch evaluates.

Model answer:

Yes. The case value is an expression that is evaluated and compared with === to the switch expression. A well-known pattern is switch (true), where each case holds a boolean expression:

switch (true) {
  case age < 13:  return "child";
  case age < 18:  return "teen";
  default:        return "adult";
}

Each case expression is evaluated in order; the first one that equals true wins. This mimics an if-else chain. It works but is generally less idiomatic than plain if-else for range comparisons.


Q12. How would you avoid an infinite while loop during development?

Why interviewers ask: Practical debugging and defensive coding.

Model answer:

  1. Always ensure the variable the condition depends on is modified inside the loop body.
  2. Use < or <= instead of !== when incrementing by a step (avoids skipping the exact value).
  3. Add a safety counter: if (iterations++ > MAX_ITERATIONS) break; -- remove it once the logic is verified.
  4. In browser DevTools, use breakpoints or debugger statements to step through iterations.
  5. Write unit tests that exercise edge cases (zero iterations, one iteration, boundary values).

Advanced (Q13--Q18)

Q13. What is the time complexity of nested loops? How do you analyze it?

Why interviewers ask: Algorithm analysis -- foundational for coding interviews.

Model answer:

A single loop over n elements is O(n). Two nested loops, each over n elements, are O(n^2). The general rule: multiply the iteration counts of nested loops. If the outer loop runs n times and the inner loop runs m times, the total is O(n * m). However, if the inner loop's range depends on the outer loop variable (e.g., j goes from i to n), the total may be O(n^2 / 2), which simplifies to O(n^2). Three nested loops would be O(n^3). When possible, replace a nested loop with a hash set or hash map lookup to reduce O(n^2) to O(n).


Q14. Explain how for...of works under the hood (the iterator protocol).

Why interviewers ask: Tests understanding of JavaScript internals and custom iterables.

Model answer:

for...of calls the object's [Symbol.iterator]() method, which returns an iterator -- an object with a next() method. Each call to next() returns { value, done }. The loop calls next() repeatedly until done is true. Arrays, strings, Maps, Sets, and generators all implement [Symbol.iterator]. This is why for...of does not work on plain objects (they have no default iterator). You can make any object iterable by implementing [Symbol.iterator]:

const range = {
  from: 1,
  to: 5,
  [Symbol.iterator]() {
    let current = this.from;
    const last = this.to;
    return {
      next() {
        return current <= last
          ? { value: current++, done: false }
          : { done: true };
      }
    };
  }
};

for (const n of range) console.log(n);  // 1, 2, 3, 4, 5

Q15. Why does switch use strict equality (===)? What pitfalls does this create?

Why interviewers ask: Edge-case awareness.

Model answer:

The spec defines switch case matching as === to avoid the confusing coercion rules of ==. Pitfalls: (1) switch (inputFromDOM) where the input is a string will not match number cases (case 2: vs "2"). You must convert first: switch (Number(input)). (2) NaN === NaN is false, so case NaN: will never match. Use if (Number.isNaN(x)) instead. (3) Objects are compared by reference, so case someObj: only matches the exact same object reference, not a structurally identical one.


Q16. Compare loop performance: for, for...of, while, and array methods (forEach, map).

Why interviewers ask: Performance-sensitive code decisions.

Model answer:

In modern JS engines, the performance differences between loop types are negligible for most workloads -- the engine optimizes heavily. That said:

LoopOverheadNotes
for (let i...)LowestDirect index access, no iterator object
whileSame as forFunctionally equivalent
for...ofSlight overheadCreates an iterator object; optimized away for arrays in V8
forEachHigherFunction call per iteration, cannot break
map/filterHigherCreates a new array, function call per iteration

For hot loops over millions of elements, classic for or while may be measurably faster. For typical application code, readability should drive the choice. Premature optimization is counterproductive -- profile first, optimize later.


Q17. Implement FizzBuzz without using if, else, switch, or the ternary operator.

Why interviewers ask: Creative problem solving and language fluency.

Model answer:

for (let i = 1; i <= 100; i++) {
  const output = ["Fizz"][i % 3] || "";
  const buzz = ["Buzz"][i % 5] || "";
  console.log(output + buzz || i);
}

How it works: ["Fizz"][0] is "Fizz", ["Fizz"][1] is undefined, ["Fizz"][2] is undefined. When i % 3 === 0, we get "Fizz"; otherwise undefined, which || "" converts to empty string. Same logic for Buzz. If both are empty, "" || i prints the number. This uses array indexing and short-circuit evaluation to avoid explicit conditionals.

Another approach using an object lookup:

for (let i = 1; i <= 100; i++) {
  const map = { 0: "FizzBuzz", 3: "Fizz", 5: "Buzz", 6: "Fizz", 9: "Fizz", 10: "Buzz", 12: "Fizz" };
  console.log(map[i % 15] ?? i);
}
// Note: this approach needs all 15 remainders mapped -- shown partially for brevity.

Q18. What is tail call optimization (TCO)? How does it relate to loops?

Why interviewers ask: Deep language knowledge; functional programming crossover.

Model answer:

Tail call optimization (TCO) is when the engine reuses the current stack frame for a function call that is the last action in a function (a "tail call"). This allows recursive functions to run in constant stack space, effectively making recursion as efficient as a loop. ES6 specifies TCO, but only Safari implements it as of 2026. Other engines (V8, SpiderMonkey) have not adopted it due to concerns about debugging and stack traces.

Practical impact: In JavaScript, if you need a loop-like pattern with many iterations, use an actual loop rather than relying on tail-recursive functions. Convert recursion to iteration when stack depth is a concern:

// Recursive (may overflow for large n)
function factorial(n) {
  if (n <= 1) return 1;
  return n * factorial(n - 1);
}

// Iterative (safe)
function factorial(n) {
  let result = 1;
  for (let i = 2; i <= n; i++) result *= i;
  return result;
}

Quick-fire

#QuestionOne-line answer
1Is [] truthy or falsy?Truthy -- all objects are truthy
2switch equality type?=== (strict)
3for...of on a plain object?TypeError -- no default iterator
4continue in while skips?Rest of current iteration body
5break in a switch inside a for loop?Exits the switch, not the loop
6var in for loop + closure?All closures see the final value
7do...while minimum runs?1
8for...in key type?String
9Nested ternaries: good practice?No -- use if/else
10Label syntax?label: for (...) then break label

<-- Back to 1.19 -- Conditionals and Loops (README)