JavaScript Control Flow & Logic

By Seokhyeon Byun 13 min read

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";

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}`);
}

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: "/api",
  },
};

// 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";

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";
}