Conditionals & Control Flow in JavaScript

Control flow is the order in which statements are executed in a program. Conditional statements and control structures allow you to control this flow based on conditions, making your code dynamic and responsive to different situations.

Boolean Logic Fundamentals

Before diving into conditionals, it's important to understand boolean logic, which forms the foundation of all conditional statements.

// Boolean values
let isTrue = true;
let isFalse = false;

// Truthy and falsy values
// The following values are always falsy:
false
0 (zero)
'' or "" (empty string)
null
undefined
NaN (Not a Number)

// Everything else is truthy, including:
true
'0' (string containing zero)
'false' (string containing the text "false")
[] (empty array)
{} (empty object)
function(){} (empty function)

// Testing truthiness
if ("Hello") {
console.log("'Hello' is truthy"); // This will execute
}

if (0) {
console.log("This won't execute because 0 is falsy");
}

The if Statement

The if statement is the most basic conditional statement. It executes a block of code if a specified condition is true.

// Basic if statement
let age = 18;

if (age >= 18) {
console.log("You are an adult."); // This will execute if age is 18 or more
}

// if with multiple conditions using logical operators
let hasLicense = true;

if (age >= 18 && hasLicense) {
console.log("You can drive."); // This will execute if both conditions are true
}

// Nested if statements
let score = 85;

if (score >= 60) {
console.log("You passed!");

if (score >= 80) {
console.log("You got an excellent grade!");
}
}

The if...else Statement

The if...else statement executes one block of code if a condition is true and another block if the condition is false.

// Basic if...else statement
let age = 16;

if (age >= 18) {
console.log("You are an adult.");
} else {
console.log("You are a minor."); // This will execute
}

// if...else with complex conditions
let temperature = 22;

if (temperature > 30) {
console.log("It's hot outside!");
} else {
console.log("The weather is pleasant."); // This will execute
}

// Using if...else for assignment
let time = 14; // 24-hour format
let greeting;

if (time < 12) {
greeting = "Good morning!";
} else {
greeting = "Good afternoon!"; // This will be assigned
}

console.log(greeting); // "Good afternoon!"

The if...else if...else Statement

The if...else if...else statement allows you to check multiple conditions and execute different blocks of code accordingly.

// Basic if...else if...else statement
let score = 85;

if (score >= 90) {
console.log("Grade: A");
} else if (score >= 80) {
console.log("Grade: B"); // This will execute
} else if (score >= 70) {
console.log("Grade: C");
} else if (score >= 60) {
console.log("Grade: D");
} else {
console.log("Grade: F");
}

// Time of day example
let hour = 15; // 24-hour format

if (hour < 12) {
console.log("Good morning!");
} else if (hour < 18) {
console.log("Good afternoon!"); // This will execute
} else if (hour < 22) {
console.log("Good evening!");
} else {
console.log("Good night!");
}

// Multiple conditions in each branch
let day = "Monday";
let isHoliday = false;

if (day === "Saturday" || day === "Sunday") {
console.log("It's the weekend!");
} else if (isHoliday) {
console.log("It's a holiday!");
} else {
console.log("It's a weekday."); // This will execute
}

Ternary Operator

The ternary operator is a shorthand for the if...else statement. It's a concise way to assign a value based on a condition.

// Basic ternary operator
let age = 20;
let status = age >= 18 ? "Adult" : "Minor";
console.log(status); // "Adult"

// Ternary operator with expressions
let score = 75;
let result = score >= 60 ? "Pass" : "Fail";
console.log(result); // "Pass"

// Nested ternary operators (use with caution for readability)
let grade = score >= 90 ? "A" :
score >= 80 ? "B" :
score >= 70 ? "C" :
score >= 60 ? "D" : "F";
console.log(grade); // "C"

// Ternary operator in template literals
let age2 = 16;
console.log(`You are ${age2 >= 18 ? "an adult" : "a minor"}.`); // "You are a minor."

// Ternary operator for conditional execution (not just assignment)
let isLoggedIn = true;
isLoggedIn ? console.log("Welcome back!") : console.log("Please log in."); // "Welcome back!"

The switch Statement

The switch statement evaluates an expression and executes the corresponding case block. It's often cleaner than multiple if...else if statements when comparing a single value against multiple possible values.

// Basic switch statement
let day = 3;
let dayName;

switch (day) {
case 1:
dayName = "Monday";
break;
case 2:
dayName = "Tuesday";
break;
case 3:
dayName = "Wednesday"; // This will be assigned
break;
case 4:
dayName = "Thursday";
break;
case 5:
dayName = "Friday";
break;
case 6:
dayName = "Saturday";
break;
case 7:
dayName = "Sunday";
break;
default:
dayName = "Invalid day";
}

console.log(dayName); // "Wednesday"

// Switch with fall-through (multiple cases sharing the same code)
let month = "February";
let days;

switch (month) {
case "January":
case "March":
case "May":
case "July":
case "August":
case "October":
case "December":
days = 31;
break;
case "April":
case "June":
case "September":
case "November":
days = 30;
break;
case "February":
days = 28; // This will be assigned
break;
default:
days = "Invalid month";
}

console.log(`${month} has ${days} days.`); // "February has 28 days."

// Switch with expressions in cases
let grade = "B";
let feedback;

switch (grade) {
case "A":
feedback = "Excellent work!";
break;
case "B":
feedback = "Good job!"; // This will be assigned
break;
case "C":
feedback = "Satisfactory.";
break;
case "D":
feedback = "Needs improvement.";
break;
case "F":
feedback = "Failed.";
break;
default:
feedback = "Invalid grade.";
}

console.log(feedback); // "Good job!"

Important: The break Statement

The break statement is crucial in switch statements. Without it, execution will "fall through" to the next case, regardless of whether that case matches the expression. This can lead to unexpected behavior if not used intentionally.

Always include a break statement at the end of each case unless you specifically want fall-through behavior.

Logical Short-Circuit Evaluation

JavaScript's logical operators (&& and ||) use short-circuit evaluation, which can be leveraged for conditional execution of code.

// Logical AND (&&) short-circuit
// If the first operand is falsy, the second operand is not evaluated
let isLoggedIn = false;
let userName = "John";

// This will not execute getUserData() because isLoggedIn is false
isLoggedIn && getUserData(); // Assuming getUserData is a function

// Logical OR (||) short-circuit
// If the first operand is truthy, the second operand is not evaluated
let cachedData = null;
let freshData = "New data";

// This will evaluate to "New data" because cachedData is null (falsy)
let data = cachedData || freshData;
console.log(data); // "New data"

// Using || for default values
function greet(name) {
// If name is undefined or empty, use "Guest"
name = name || "Guest";
return `Hello, ${name}!`;
}

console.log(greet("John")); // "Hello, John!"
console.log(greet()); // "Hello, Guest!"

// Using && for conditional execution
let isAdmin = true;

// This will execute because isAdmin is true
isAdmin && console.log("Admin privileges granted."); // "Admin privileges granted."

// Nullish coalescing operator (??) - ES2020
// Similar to ||, but only considers null and undefined as falsy
let count = 0; // This is falsy, but it's a valid value
let defaultCount = 10;

// With ||, 0 is considered falsy, so defaultCount is used
let result1 = count || defaultCount;
console.log(result1); // 10

// With ??, 0 is not nullish, so count is used
let result2 = count ?? defaultCount;
console.log(result2); // 0

Exception Handling with try...catch

The try...catch statement allows you to handle errors gracefully, preventing them from crashing your program.

// Basic try...catch
try {
// Code that might throw an error
let result = 10 / 0; // This doesn't throw an error in JavaScript (returns Infinity)
console.log(result); // Infinity

// Let's cause an error
let obj = null;
console.log(obj.property); // This will throw a TypeError
} catch (error) {
// Code to handle the error
console.log("An error occurred:");
console.log(error.name); // "TypeError"
console.log(error.message); // "Cannot read property 'property' of null"
}

// Code after try...catch continues to execute
console.log("Program continues running.");

// try...catch...finally
try {
console.log("Try block executed");
throw new Error("Something went wrong!"); // Manually throw an error
} catch (error) {
console.log("Catch block executed");
console.log(error.message); // "Something went wrong!"
} finally {
// This block always executes, regardless of whether an error occurred
console.log("Finally block executed");
}

// Catching specific error types
try {
let json = '{"name":"John", "age":30'; // Invalid JSON (missing closing brace)
let user = JSON.parse(json); // This will throw a SyntaxError
} catch (error) {
if (error instanceof SyntaxError) {
console.log("JSON Syntax Error:", error.message);
} else {
// Re-throw errors that we don't know how to handle
throw error;
}
}

// The throw statement
function divide(a, b) {
if (b === 0) {
throw new Error("Division by zero is not allowed");
}
return a / b;
}

try {
let result = divide(10, 0);
console.log(result); // This line won't execute
} catch (error) {
console.log(error.message); // "Division by zero is not allowed"
}

Advanced Control Flow Techniques

JavaScript offers several advanced techniques for controlling the flow of your program.

// 1. Label statements
// Labels can be used with break and continue statements
outerLoop: for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (i === 1 && j === 1) {
console.log(`Breaking out of both loops at i=${i}, j=${j}`);
break outerLoop; // Breaks out of both loops
}
console.log(`i=${i}, j=${j}`);
}
}

// 2. The return statement
// return immediately exits a function
function findFirstNegative(numbers) {
for (let i = 0; i < numbers.length; i++) {
if (numbers[i] < 0) {
return numbers[i]; // Exits the function immediately
}
}
return null; // No negative numbers found
}

console.log(findFirstNegative([5, 3, -2, 8, -7])); // -2

// 3. The break statement
// break exits the current loop or switch statement
for (let i = 0; i < 10; i++) {
if (i === 5) {
break; // Exits the loop when i is 5
}
console.log(i); // Outputs 0, 1, 2, 3, 4
}

// 4. The continue statement
// continue skips the current iteration and continues with the next one
for (let i = 0; i < 5; i++) {
if (i === 2) {
continue; // Skips iteration when i is 2
}
console.log(i); // Outputs 0, 1, 3, 4
}

// 5. Optional chaining operator (?.) - ES2020
// Safely accesses nested properties without throwing errors
let user = {
name: "John",
address: {
city: "New York"
}
};

let user2 = {
name: "Jane"
// No address property
};

// Without optional chaining
// This would throw an error for user2
// console.log(user2.address.city); // TypeError

// With optional chaining
console.log(user.address?.city); // "New York"
console.log(user2.address?.city); // undefined (no error)

// 6. Conditional (ternary) operator chaining
let score = 85;
let grade = score >= 90 ? "A" :
score >= 80 ? "B" :
score >= 70 ? "C" :
score >= 60 ? "D" : "F";
console.log(grade); // "B"

Best Practices for Control Flow

Following these best practices will make your code more readable, maintainable, and less prone to errors.

// 1. Use block statements with curly braces, even for single-line blocks
// Bad practice
if (condition) doSomething();

// Good practice
if (condition) {
doSomething();
}

// 2. Be careful with automatic semicolon insertion
// Bad practice (returns undefined due to automatic semicolon insertion)
function badReturn() {
return
{
value: 42
};
}

// Good practice
function goodReturn() {
return {
value: 42
};
}

// 3. Avoid deeply nested if statements
// Bad practice
function deeplyNested(a, b, c) {
if (a) {
if (b) {
if (c) {
return "All conditions met";
} else {
return "A and B met, C failed";
}
} else {
return "A met, B failed";
}
} else {
return "A failed";
}
}

// Good practice - early returns
function earlyReturns(a, b, c) {
if (!a) return "A failed";
if (!b) return "A met, B failed";
if (!c) return "A and B met, C failed";
return "All conditions met";
}

// 4. Use switch for multiple conditions on the same variable
// Bad practice
function getMonthName(month) {
if (month === 1) return "January";
else if (month === 2) return "February";
else if (month === 3) return "March";
// ... and so on
}

// Good practice
function getMonthNameBetter(month) {
switch (month) {
case 1: return "January";
case 2: return "February";
case 3: return "March";
// ... and so on
default: return "Invalid month";
}
}

// 5. Use object literals for complex conditionals
// Bad practice
function getDiscount(customerType) {
if (customerType === "regular") return 0;
else if (customerType === "bronze") return 0.05;
else if (customerType === "silver") return 0.1;
else if (customerType === "gold") return 0.15;
else if (customerType === "platinum") return 0.2;
else return 0;
}

// Good practice
function getDiscountBetter(customerType) {
const discounts = {
regular: 0,
bronze: 0.05,
silver: 0.1,
gold: 0.15,
platinum: 0.2
};
return discounts[customerType] || 0;
}

Control Flow in Asynchronous JavaScript

Asynchronous JavaScript introduces additional control flow challenges and patterns.

// 1. Callback pattern (traditional)
function fetchData(callback) {
setTimeout(() => {
const data = { name: "John", age: 30 };
callback(null, data); // First parameter is error (null means no error)
}, 1000);
}

fetchData((error, data) => {
if (error) {
console.error("Error:", error);
} else {
console.log("Data:", data);
}
});

// 2. Promise-based control flow
function fetchDataPromise() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const success = true; // Simulate success/failure
if (success) {
resolve({ name: "John", age: 30 });
} else {
reject(new Error("Failed to fetch data"));
}
}, 1000);
});
}

// Using Promise with then/catch
fetchDataPromise()
.then(data => {
console.log("Data:", data);
return processData(data); // Return another promise for chaining
})
.then(result => {
console.log("Processed result:", result);
})
.catch(error => {
console.error("Error:", error);
})
.finally(() => {
console.log("Operation completed");
});

// 3. Async/await pattern (ES2017)
async function fetchAndProcessData() {
try {
// await pauses execution until the promise resolves
const data = await fetchDataPromise();
console.log("Data:", data);

const result = await processData(data);
console.log("Processed result:", result);

return result;
} catch (error) {
// Handles any errors in the try block
console.error("Error:", error);
} finally {
console.log("Operation completed");
}
}

// 4. Conditional execution with Promises
async function conditionalFetch(shouldFetch) {
if (shouldFetch) {
try {
const data = await fetchDataPromise();
return data;
} catch (error) {
console.error("Fetch error:", error);
return null;
}
} else {
return { name: "Default", age: 0 };
}
}

Practice Exercise

Create a JavaScript file that demonstrates different control flow techniques. Try to solve these challenges:

  1. Write a function that determines the grade (A, B, C, D, or F) based on a numeric score using if...else if statements
  2. Rewrite the same function using a switch statement
  3. Create a function that checks if a year is a leap year (divisible by 4, but not by 100 unless also divisible by 400)
  4. Write a function that finds the largest number in an array using control flow statements
  5. Create a password strength checker that evaluates a password based on length, use of special characters, numbers, and mixed case

Common Pitfalls in Control Flow

  • Equality confusion: Using == instead of === can lead to unexpected type coercion
  • Missing break statements: Forgetting break statements in switch cases causes fall-through behavior
  • Unreachable code: Code after return, break, or continue statements will never execute
  • Complex conditions: Overly complex boolean expressions can be hard to understand and maintain
  • Deeply nested conditionals: Excessive nesting makes code hard to follow and maintain
  • Misplaced semicolons: Especially with return statements, can cause unexpected behavior
  • Forgetting that 0, empty strings, null, undefined, and NaN are falsy: This can lead to unexpected conditional behavior

Advanced Topic: State Machines

For complex control flow scenarios, state machines provide a structured approach to managing application state and transitions.

// Simple state machine for a traffic light
class TrafficLight {
constructor() {
// Initial state
this.state = "red";

// State transition rules
this.transitions = {
red: "green",
green: "yellow",
yellow: "red"
};

// Actions for each state
this.actions = {
red: () => console.log("Stop!"),
green: () => console.log("Go!"),
yellow: () => console.log("Prepare to stop!")
};
}

// Change to the next state
next() {
this.state = this.transitions[this.state];
this.actions[this.state]();
return this.state;
}

// Get current state
getState() {
return this.state;
}
}

// Usage
const light = new TrafficLight();
console.log(light.getState()); // "red"
light.next(); // "Go!"
console.log(light.getState()); // "green"
light.next(); // "Prepare to stop!"
console.log(light.getState()); // "yellow"
light.next(); // "Stop!"
console.log(light.getState()); // "red"