JavaScript Control Flow & Logic

· Seokhyeon Byun

Note: This post is based on my old programming study notes when I taught myself.

Conditional Statements

If…Else Statements

The foundation of conditional logic in JavaScript.

// Basic if statement
const age = 25;
if (age >= 18) {
  console.log("You are an adult");
}

// If...else
const score = 85;
if (score >= 90) {
  console.log("Grade: A");
} else {
  console.log("Grade: B or lower");
}

// Multiple conditions with else if
function getGrade(score) {
  if (score >= 90) {
    return "A";
  } else if (score >= 80) {
    return "B";
  } else if (score >= 70) {
    return "C";
  } else if (score >= 60) {
    return "D";
  } else {
    return "F";
  }
}

// Nested conditions
const user = { role: "admin", isActive: true };
if (user.role === "admin") {
  if (user.isActive) {
    console.log("Admin access granted");
  } else {
    console.log("Admin account disabled");
  }
} else {
  console.log("Regular user access");
}

// Block statements with multiple operations
const temperature = 30;
if (temperature > 25) {
  console.log("It's hot outside");
  console.log("Consider wearing light clothes");
  console.log("Stay hydrated");
}

Key Points:

  • Condition must evaluate to truthy/falsy value
  • Use curly braces {} for multiple statements
  • Nested if statements can become complex - consider alternatives

Ternary Operator

Concise conditional expressions for simple if…else logic.

// Basic ternary syntax: condition ? expressionA : expressionB
const age = 20;
const status = age >= 18 ? "adult" : "minor";
console.log(status); // "adult"

// Ternary vs if...else comparison
// Ternary (expression - returns value)
const message = isLoggedIn ? "Welcome back!" : "Please log in";

// If...else (statement - performs action)
let message;
if (isLoggedIn) {
  message = "Welcome back!";
} else {
  message = "Please log in";
}

// Nested ternary (use sparingly)
const score = 85;
const grade = score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : "F";

// Practical examples
function formatPrice(price, hasDiscount) {
  return hasDiscount ? `$${price * 0.8} (20% off)` : `$${price}`;
}

// Ternary in JSX/template contexts
const Button = ({ isLoading }) => (
  <button disabled={isLoading}>{isLoading ? "Loading..." : "Submit"}</button>
);

// Ternary with function calls
const result = isValid ? processData() : handleError();

// Multiple conditions (consider readability)
const accessLevel = isAdmin
  ? "full"
  : isModerator
  ? "moderate"
  : isUser
  ? "basic"
  : "none";

Interview Question: When should you use ternary operator vs if…else?

Answer: Use ternary for simple conditions that return values (expressions). Use if…else for complex logic, multiple statements, or when readability is important.


Switch Statements

Efficient multi-way branching for comparing a single value against multiple cases.

// Basic switch statement
function getDayName(dayNumber) {
  switch (dayNumber) {
    case 0:
      return "Sunday";
    case 1:
      return "Monday";
    case 2:
      return "Tuesday";
    case 3:
      return "Wednesday";
    case 4:
      return "Thursday";
    case 5:
      return "Friday";
    case 6:
      return "Saturday";
    default:
      return "Invalid day";
  }
}

// Fall-through behavior (intentional)
function getQuarter(month) {
  switch (month) {
    case 1:
    case 2:
    case 3:
      return "Q1";
    case 4:
    case 5:
    case 6:
      return "Q2";
    case 7:
    case 8:
    case 9:
      return "Q3";
    case 10:
    case 11:
    case 12:
      return "Q4";
    default:
      return "Invalid month";
  }
}

// Fall-through demonstration
const foo = 0;
switch (foo) {
  case -1:
    console.log("negative 1");
    break;
  case 0: // Execution starts here
    console.log(0);
  // No break! Falls through
  case 1: // This also executes
    console.log(1);
    break;
  case 2:
    console.log(2);
    break;
  default:
    console.log("default");
}
// Output: 0, 1

// Switch with expressions and variables
function handleUserAction(action, user) {
  switch (action.type) {
    case "LOGIN":
      return { ...user, isLoggedIn: true };
    case "LOGOUT":
      return { ...user, isLoggedIn: false };
    case "UPDATE_PROFILE":
      return { ...user, ...action.payload };
    case "DELETE_ACCOUNT":
      if (user.role === "admin") {
        throw new Error("Cannot delete admin account");
      }
      return null;
    default:
      console.warn("Unknown action type:", action.type);
      return user;
  }
}

// Switch vs if...else performance
// Switch is generally faster for many conditions with simple equality checks
function processHttpStatus(status) {
  switch (status) {
    case 200:
      return "OK";
    case 201:
      return "Created";
    case 400:
      return "Bad Request";
    case 401:
      return "Unauthorized";
    case 403:
      return "Forbidden";
    case 404:
      return "Not Found";
    case 500:
      return "Internal Server Error";
    default:
      return `Unknown status: ${status}`;
  }
}

Key Points:

  • Uses strict equality (===) for comparisons
  • Each case needs break to prevent fall-through
  • default case is optional but recommended
  • More efficient than if…else chains for many conditions

Loop Statements

For Loops

Traditional counting loops with initialization, condition, and increment.

// Basic for loop
for (let i = 0; i < 5; i++) {
  console.log(`Iteration ${i}`);
}

// Different initialization patterns
for (let i = 0, j = 10; i < j; i++, j--) {
  console.log(`i: ${i}, j: ${j}`);
}

// Decrementing loop
for (let i = 10; i > 0; i--) {
  console.log(`Countdown: ${i}`);
}

// Looping through arrays
const fruits = ["apple", "banana", "orange"];
for (let i = 0; i < fruits.length; i++) {
  console.log(`${i}: ${fruits[i]}`);
}

// Complex initialization with ternary operator
for (let i = ("start" in window) ? window.start : 0; i < 9; i++) {
  console.log(i);
}

// Infinite loop (be careful!)
// for (;;) {
//   console.log("This runs forever!");
//   break; // Need an exit condition
// }

// Nested loops for 2D operations
const matrix = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9],
];

for (let row = 0; row < matrix.length; row++) {
  for (let col = 0; col < matrix[row].length; col++) {
    console.log(`matrix[${row}][${col}] = ${matrix[row][col]}`);
  }
}

// For...in loop (iterates over object properties)
const person = { name: "John", age: 30, city: "New York" };
for (const key in person) {
  console.log(`${key}: ${person[key]}`);
}

// For...of loop (iterates over iterable values)
const colors = ["red", "green", "blue"];
for (const color of colors) {
  console.log(color);
}

// For...of with index using entries()
for (const [index, color] of colors.entries()) {
  console.log(`${index}: ${color}`);
}

// For...of with objects (using Object.entries)
for (const [key, value] of Object.entries(person)) {
  console.log(`${key}: ${value}`);
}

While Loops

Condition-based loops that continue while a condition is true.

// Basic while loop
let count = 0;
while (count < 5) {
  console.log(`Count: ${count}`);
  count++;
}

// While loop with complex condition
let attempts = 0;
let success = false;
while (!success && attempts < 3) {
  console.log(`Attempt ${attempts + 1}`);
  success = Math.random() > 0.5; // Simulate random success
  attempts++;
}

// Do...while loop (executes at least once)
let userInput;
do {
  userInput = prompt("Enter a number greater than 10:");
  userInput = parseInt(userInput);
} while (userInput <= 10);

// Processing data until condition is met
const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let index = 0;
let sum = 0;
while (index < data.length && sum < 15) {
  sum += data[index];
  index++;
}
console.log(`Sum: ${sum}, stopped at index: ${index}`);

// Infinite loop with break condition
let counter = 0;
while (true) {
  console.log(`Processing item ${counter}`);
  counter++;

  if (counter >= 5) {
    break; // Exit condition
  }

  if (counter === 2) {
    continue; // Skip to next iteration
  }

  console.log(`Completed processing item ${counter - 1}`);
}

Interview Question: When would you use a while loop vs a for loop?

Answer: Use for loops when you know the number of iterations or need an index counter. Use while loops when the condition depends on dynamic state or when you don’t know the iteration count in advance.


Control Flow Modifiers

Break and Continue

Control loop execution flow with early exit or skip mechanisms.

// Break statement - exits the loop entirely
for (let i = 0; i < 10; i++) {
  if (i === 5) {
    break; // Stops loop when i equals 5
  }
  console.log(i); // Prints 0, 1, 2, 3, 4
}

// Continue statement - skips current iteration
for (let i = 0; i < 10; i++) {
  if (i % 2 === 0) {
    continue; // Skip even numbers
  }
  console.log(i); // Prints 1, 3, 5, 7, 9
}

// Practical example: Finding first match
function findUser(users, criteria) {
  for (let i = 0; i < users.length; i++) {
    const user = users[i];
    let matches = true;

    for (const key in criteria) {
      if (user[key] !== criteria[key]) {
        matches = false;
        break; // No need to check other criteria
      }
    }

    if (matches) {
      return user; // Found match, exit function
    }
  }
  return null; // No match found
}

// Break in switch statement
function processCommand(command) {
  switch (command.type) {
    case "start":
      console.log("Starting process");
      break; // Prevents fall-through
    case "stop":
      console.log("Stopping process");
      break;
    case "pause":
      console.log("Pausing process");
      break;
    default:
      console.log("Unknown command");
  }
}

// Continue with conditions
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const processedNumbers = [];

for (const num of numbers) {
  // Skip negative numbers
  if (num < 0) continue;

  // Skip even numbers
  if (num % 2 === 0) continue;

  // Skip numbers greater than 7
  if (num > 7) continue;

  processedNumbers.push(num * 2);
}
console.log(processedNumbers); // [2, 6, 14]

Recursion

Functions that call themselves to solve problems by breaking them into smaller subproblems.

// Basic recursion example - factorial
function factorial(n) {
  // Base case
  if (n <= 1) {
    return 1;
  }
  // Recursive case
  return n * factorial(n - 1);
}

console.log(factorial(5)); // 120 (5 * 4 * 3 * 2 * 1)

// Fibonacci sequence
function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

// Optimized fibonacci with memoization
function fibonacciMemo(n, memo = {}) {
  if (n in memo) return memo[n];
  if (n <= 1) return n;

  memo[n] = fibonacciMemo(n - 1, memo) + fibonacciMemo(n - 2, memo);
  return memo[n];
}

// Tree traversal
const tree = {
  value: 1,
  children: [
    {
      value: 2,
      children: [
        { value: 4, children: [] },
        { value: 5, children: [] },
      ],
    },
    {
      value: 3,
      children: [{ value: 6, children: [] }],
    },
  ],
};

function traverseTree(node, depth = 0) {
  console.log("  ".repeat(depth) + node.value);

  for (const child of node.children) {
    traverseTree(child, depth + 1);
  }
}

// Array flattening
function flattenArray(arr) {
  const result = [];

  for (const item of arr) {
    if (Array.isArray(item)) {
      result.push(...flattenArray(item)); // Recursive call
    } else {
      result.push(item);
    }
  }

  return result;
}

console.log(flattenArray([1, [2, [3, 4], 5], 6])); // [1, 2, 3, 4, 5, 6]

// Binary search (recursive)
function binarySearch(arr, target, left = 0, right = arr.length - 1) {
  if (left > right) return -1; // Not found

  const mid = Math.floor((left + right) / 2);

  if (arr[mid] === target) return mid;
  if (arr[mid] > target) return binarySearch(arr, target, left, mid - 1);
  return binarySearch(arr, target, mid + 1, right);
}

// Tail recursion example
function countdown(n) {
  if (n <= 0) {
    console.log("Done!");
    return;
  }

  console.log(n);
  countdown(n - 1); // Tail call
}

// Converting recursion to iteration (stack approach)
function factorialIterative(n) {
  if (n <= 1) return 1;

  let result = 1;
  for (let i = 2; i <= n; i++) {
    result *= i;
  }
  return result;
}

Key Points:

  • Every recursion needs a base case to prevent infinite loops
  • Recursive solutions can be elegant but may have performance costs
  • Consider memoization for overlapping subproblems
  • Some recursive solutions can be converted to iterative ones

Short Circuit Evaluation

Logical operators that stop evaluation once the result is determined.

// Logical AND (&&) - short circuits on false
console.log(true && true); // true
console.log(true && false); // false
console.log(false && true); // false (doesn't evaluate second operand)

// Practical AND usage - conditional execution
const user = { name: "John", isAdmin: true };
user.isAdmin && console.log("Admin access granted"); // Executes

const data = null;
data && data.process(); // Doesn't execute data.process() - prevents error

// Logical OR (||) - short circuits on true
console.log(true || false); // true (doesn't evaluate second operand)
console.log(false || true); // true
console.log(false || false); // false

// Practical OR usage - default values
function greet(name) {
  name = name || "Guest"; // Use 'Guest' if name is falsy
  return `Hello, ${name}!`;
}

// Modern alternative with nullish coalescing
function greetModern(name) {
  name = name ?? "Guest"; // Use 'Guest' only if name is null/undefined
  return `Hello, ${name}!`;
}

// Chaining with short circuit evaluation
const config = {
  api: {
    baseUrl: "https://api.example.com",
  },
};

// Safe property access (pre-optional chaining)
const baseUrl = config && config.api && config.api.baseUrl;

// Modern equivalent with optional chaining
const baseUrlModern = config?.api?.baseUrl;

// Function calls with short circuit
function expensiveOperation() {
  console.log("Performing expensive operation...");
  return "result";
}

const shouldProcess = false;
const result = shouldProcess && expensiveOperation(); // expensiveOperation() not called

// Short circuit in conditions
function processUser(user) {
  // Validate user exists and has required properties
  if (!user || !user.id || !user.name) {
    return "Invalid user";
  }

  // Process user...
  return "User processed";
}

// Multiple conditions with short circuit
function canAccessFeature(user) {
  return (
    user &&
    user.isActive &&
    user.subscription &&
    user.subscription.plan === "premium" &&
    new Date(user.subscription.expiryDate) > new Date()
  );
}

// Short circuit for caching
let expensiveResult;
function getExpensiveResult() {
  return expensiveResult || (expensiveResult = performExpensiveCalculation());
}

function performExpensiveCalculation() {
  console.log("Calculating...");
  return "expensive result";
}

// Conditional assignment patterns
const theme = userPreferences.theme || systemTheme || "light";
const timeout = options.timeout || config.defaultTimeout || 5000;

// Guard clauses using short circuit
function divide(a, b) {
  b === 0 && console.error("Division by zero!");
  return b === 0 ? undefined : a / b;
}

// React-like conditional rendering pattern
const isLoading = false;
const hasError = false;
const data = ["item1", "item2"];

// Conditional rendering logic
const content =
  (hasError && "Error occurred!") ||
  (isLoading && "Loading...") ||
  (data.length > 0 && data.map((item) => item)) ||
  "No data available";

Interview Question: Explain how short circuit evaluation works and provide practical use cases.

Answer: Short circuit evaluation stops evaluating logical expressions once the result is determined. With &&, if the left side is falsy, the right side isn’t evaluated. With ||, if the left side is truthy, the right side isn’t evaluated. This is useful for conditional execution, default values, and preventing errors when accessing potentially undefined properties.


Advanced Control Flow Patterns

Switch Expression Pattern (Modern JavaScript)

// Traditional switch
function getResponseMessage(status) {
  switch (status) {
    case 200:
      return "Success";
    case 404:
      return "Not Found";
    case 500:
      return "Server Error";
    default:
      return "Unknown Status";
  }
}

// Object-based switch alternative
const statusMessages = {
  200: "Success",
  404: "Not Found",
  500: "Server Error",
};

function getResponseMessageAlt(status) {
  return statusMessages[status] || "Unknown Status";
}

// Map-based switch for complex cases
const actionHandlers = new Map([
  ["LOGIN", (user) => ({ ...user, isLoggedIn: true })],
  ["LOGOUT", (user) => ({ ...user, isLoggedIn: false })],
  ["UPDATE", (user, data) => ({ ...user, ...data })],
]);

function handleAction(action, user, data) {
  const handler = actionHandlers.get(action);
  return handler ? handler(user, data) : user;
}

Guard Clauses Pattern

// Instead of nested if statements
function processUserBad(user) {
  if (user) {
    if (user.isActive) {
      if (user.permissions) {
        if (user.permissions.canRead) {
          // Process user
          return "User processed";
        } else {
          return "No read permission";
        }
      } else {
        return "No permissions";
      }
    } else {
      return "User inactive";
    }
  } else {
    return "No user provided";
  }
}

// Use guard clauses for early returns
function processUserGood(user) {
  if (!user) return "No user provided";
  if (!user.isActive) return "User inactive";
  if (!user.permissions) return "No permissions";
  if (!user.permissions.canRead) return "No read permission";

  // Process user
  return "User processed";
}