JavaScript and TypeScript course ยท Module 7: Advanced TypeScript

Avoiding Deep Nesting - Early Returns

12 min read ยท
In this lesson6

Dr. Ian Malcolm, sitting in the Jurassic Park control center, observes the code written by the IT team. "Look at this," he says, pointing at the screen. "It's like walking through a maze full of dinosaurs - every if is another turn, every else is a dead end. Chaos finds a way... but we shouldn't make the path harder!"

The Problem: Arrow Anti-Pattern

One of the most common problems in beginner programmers' code is the so-called "Arrow Anti-Pattern" or "Pyramid of Doom" - code that nests deeper and deeper, creating a characteristic arrow shape:

// BAD: Arrow Anti-Pattern - code nested like a pyramid
function checkDinosaurSafety(dinosaur) {
  if (dinosaur) {
    if (dinosaur.isAlive) {
      if (dinosaur.enclosure) {
        if (dinosaur.enclosure.fence) {
          if (dinosaur.enclosure.fence.isActive) {
            if (dinosaur.health > 50) {
              if (dinosaur.aggressionLevel < 7) {
                if (dinosaur.lastFed) {
                  const hoursSinceFeeding = (Date.now() - dinosaur.lastFed) / 3600000;
                  if (hoursSinceFeeding < 12) {
                    return 'Safe';
                  } else {
                    return 'Hungry - be careful!';
                  }
                } else {
                  return 'No feeding data';
                }
              } else {
                return 'High aggression level!';
              }
            } else {
              return 'Low health level';
            }
          } else {
            return 'ALARM: Fence inactive!';
          }
        } else {
          return 'ERROR: No fence';
        }
      } else {
        return 'ERROR: No assigned enclosure';
      }
    } else {
      return 'Dinosaur deceased';
    }
  } else {
    return 'No dinosaur data';
  }
}

// This code is:
// - Hard to read (you have to count brackets)
// - Hard to understand (logic is scattered)
// - Hard to modify (adding a condition requires reorganization)
// - Error-prone (easy to get lost in the nesting)

The Solution: Early Returns (Guard Clauses)

Instead of nesting code, we use early returns - we check error conditions at the beginning and immediately exit the function. This is also called "guard clauses".

// GOOD: Early Returns - readable linear code
function checkDinosaurSafety(dinosaur) {
  // Check all error conditions right away
  if (!dinosaur) {
    return 'No dinosaur data';
  }

  if (!dinosaur.isAlive) {
    return 'Dinosaur deceased';
  }

  if (!dinosaur.enclosure) {
    return 'ERROR: No assigned enclosure';
  }

  if (!dinosaur.enclosure.fence) {
    return 'ERROR: No fence';
  }

  if (!dinosaur.enclosure.fence.isActive) {
    return 'ALARM: Fence inactive!';
  }

  if (dinosaur.health <= 50) {
    return 'Low health level';
  }

  if (dinosaur.aggressionLevel >= 7) {
    return 'High aggression level!';
  }

  if (!dinosaur.lastFed) {
    return 'No feeding data';
  }

  // Main logic at the end
  const hoursSinceFeeding = (Date.now() - dinosaur.lastFed) / 3600000;

  if (hoursSinceFeeding >= 12) {
    return 'Hungry - be careful!';
  }

  return 'Safe';
}

// This code is:
// Easy to read (linear flow)
// Easy to understand (each condition is separate)
// Easy to modify (adding a condition = one line)
// Less error-prone (no nesting)

Rules for Using Early Returns

1. Check Edge Cases and Errors First

// BAD: Main logic hidden in nesting
function processVisitorEntry(visitor, ticket) {
  if (visitor && ticket) {
    if (ticket.isValid) {
      if (ticket.hasAdultSupervision || visitor.age >= 18) {
        if (visitor.hasHealthClearance) {
          // Main logic deeply nested
          registerEntry(visitor);
          return true;
        }
      }
    }
  }
  return false;
}

// GOOD: Edge cases first
function processVisitorEntry(visitor, ticket) {
  // Input data validation
  if (!visitor || !ticket) {
    throw new Error('Missing visitor or ticket data');
  }

  // Checking conditions from most basic
  if (!ticket.isValid) {
    throw new Error('Invalid ticket');
  }

  if (visitor.age < 18 && !ticket.hasAdultSupervision) {
    throw new Error('Minor requires adult supervision');
  }

  if (!visitor.hasHealthClearance) {
    throw new Error('No health clearance');
  }

  // Main logic at the end - now we know everything is OK
  registerEntry(visitor);
  return true;
}

2. Use Negation for Error Conditions

// BAD: Positive conditions lead to nesting
function feedDinosaur(dinosaurId, foodAmount) {
  if (dinosaurs.has(dinosaurId)) {
    const dino = dinosaurs.get(dinosaurId);
    if (dino.isAlive) {
      if (foodAmount > 0) {
        if (foodAmount <= MAX_FOOD_PORTION) {
          // Feeding logic
          dino.feed(foodAmount);
          return true;
        }
      }
    }
  }
  return false;
}

// GOOD: Negating conditions allows early return
function feedDinosaur(dinosaurId, foodAmount) {
  // Check all ERROR conditions and exit immediately
  if (!dinosaurs.has(dinosaurId)) {
    throw new Error(`Dinosaur ${dinosaurId} does not exist`);
  }

  const dino = dinosaurs.get(dinosaurId);

  if (!dino.isAlive) {
    throw new Error('Cannot feed a dead dinosaur');
  }

  if (foodAmount <= 0) {
    throw new Error('Food amount must be greater than 0');
  }

  if (foodAmount > MAX_FOOD_PORTION) {
    throw new Error(`Maximum portion is ${MAX_FOOD_PORTION}kg`);
  }

  // Main logic - we know everything is OK
  dino.feed(foodAmount);
  return true;
}

3. Extract Complex Logic into Separate Functions

// BAD: Complex logic nested
function authorizeAccess(employee, sector) {
  if (employee) {
    if (employee.isActive) {
      if (employee.clearanceLevel >= sector.requiredClearance) {
        if (employee.department === sector.allowedDepartment ||
            employee.role === 'Admin' ||
            employee.temporaryAccess.includes(sector.id)) {
          if (!employee.accessRestrictions.includes(sector.id)) {
            if (isWithinAllowedHours(employee.schedule, sector.operatingHours)) {
              return grantAccess(employee, sector);
            }
          }
        }
      }
    }
  }
  return denyAccess(employee, sector);
}

// GOOD: Extract conditions into helper functions
function authorizeAccess(employee, sector) {
  if (!employee) {
    return denyAccess(null, sector, 'No employee data');
  }

  if (!employee.isActive) {
    return denyAccess(employee, sector, 'Account inactive');
  }

  if (!hasSufficientClearance(employee, sector)) {
    return denyAccess(employee, sector, 'Insufficient clearance level');
  }

  if (!isDepartmentAuthorized(employee, sector)) {
    return denyAccess(employee, sector, 'Unauthorized department');
  }

  if (hasAccessRestriction(employee, sector)) {
    return denyAccess(employee, sector, 'Access restriction');
  }

  if (!isWithinAllowedHours(employee.schedule, sector.operatingHours)) {
    return denyAccess(employee, sector, 'Outside allowed hours');
  }

  return grantAccess(employee, sector);
}

// Helper functions - each with a single responsibility
function hasSufficientClearance(employee, sector) {
  return employee.clearanceLevel >= sector.requiredClearance;
}

function isDepartmentAuthorized(employee, sector) {
  return employee.department === sector.allowedDepartment ||
         employee.role === 'Admin' ||
         employee.temporaryAccess.includes(sector.id);
}

function hasAccessRestriction(employee, sector) {
  return employee.accessRestrictions.includes(sector.id);
}

Practical Refactoring Examples

Example 1: Alarm System

// BEFORE: Nested code hard to read
function triggerAlarm(sensorData) {
  if (sensorData) {
    if (sensorData.fence) {
      if (sensorData.fence.voltage < MIN_VOLTAGE) {
        if (emergencyProtocol.isActive) {
          if (securityTeam.isAvailable) {
            if (evacuationRoutes.areOpen) {
              activateFullAlarm();
              notifySecurityTeam();
              return { status: 'active', level: 'critical' };
            } else {
              activatePartialAlarm();
              return { status: 'active', level: 'warning' };
            }
          } else {
            activateAutomatedResponse();
            return { status: 'automated', level: 'critical' };
          }
        } else {
          logIncident();
          return { status: 'logged', level: 'info' };
        }
      } else {
        return { status: 'normal', level: 'info' };
      }
    }
  }
  return { status: 'error', level: 'critical' };
}

// AFTER: Readable code with early returns
function triggerAlarm(sensorData) {
  // Data validation
  if (!sensorData || !sensorData.fence) {
    return { status: 'error', level: 'critical', message: 'No sensor data' };
  }

  // Check main condition
  if (sensorData.fence.voltage >= MIN_VOLTAGE) {
    return { status: 'normal', level: 'info', message: 'Voltage normal' };
  }

  // Low voltage detected - check protocols
  if (!emergencyProtocol.isActive) {
    logIncident();
    return { status: 'logged', level: 'info', message: 'Incident logged' };
  }

  // Protocol active - check team availability
  if (!securityTeam.isAvailable) {
    activateAutomatedResponse();
    return { status: 'automated', level: 'critical', message: 'Automated response' };
  }

  // Team available - check evacuation routes
  if (!evacuationRoutes.areOpen) {
    activatePartialAlarm();
    return { status: 'active', level: 'warning', message: 'Partial alarm' };
  }

  // Everything ready - full alarm
  activateFullAlarm();
  notifySecurityTeam();
  return { status: 'active', level: 'critical', message: 'Full alarm activated' };
}

Example 2: Form Validation

// BEFORE: Validation with many nesting levels
function validateVisitorRegistration(formData) {
  if (formData.firstName) {
    if (formData.lastName) {
      if (formData.email) {
        if (isValidEmail(formData.email)) {
          if (formData.age) {
            if (formData.age >= 5 && formData.age <= 120) {
              if (formData.emergencyContact) {
                if (formData.emergencyContact.phone) {
                  if (isValidPhone(formData.emergencyContact.phone)) {
                    if (formData.acceptedTerms) {
                      return { valid: true, data: formData };
                    } else {
                      return { valid: false, error: 'You must accept the terms' };
                    }
                  } else {
                    return { valid: false, error: 'Invalid emergency phone number' };
                  }
                } else {
                  return { valid: false, error: 'Missing emergency phone number' };
                }
              } else {
                return { valid: false, error: 'Missing emergency contact' };
              }
            } else {
              return { valid: false, error: 'Age must be between 5 and 120' };
            }
          } else {
            return { valid: false, error: 'Missing age' };
          }
        } else {
          return { valid: false, error: 'Invalid email address' };
        }
      } else {
        return { valid: false, error: 'Missing email address' };
      }
    } else {
      return { valid: false, error: 'Missing last name' };
    }
  } else {
    return { valid: false, error: 'Missing first name' };
  }
}

// AFTER: Readable validation with early returns
function validateVisitorRegistration(formData) {
  // Validate required text fields
  if (!formData.firstName) {
    return { valid: false, error: 'Missing first name' };
  }

  if (!formData.lastName) {
    return { valid: false, error: 'Missing last name' };
  }

  if (!formData.email) {
    return { valid: false, error: 'Missing email address' };
  }

  if (!isValidEmail(formData.email)) {
    return { valid: false, error: 'Invalid email address' };
  }

  // Age validation
  if (!formData.age) {
    return { valid: false, error: 'Missing age' };
  }

  if (formData.age < 5 || formData.age > 120) {
    return { valid: false, error: 'Age must be between 5 and 120' };
  }

  // Emergency contact validation
  if (!formData.emergencyContact) {
    return { valid: false, error: 'Missing emergency contact' };
  }

  if (!formData.emergencyContact.phone) {
    return { valid: false, error: 'Missing emergency phone number' };
  }

  if (!isValidPhone(formData.emergencyContact.phone)) {
    return { valid: false, error: 'Invalid emergency phone number' };
  }

  // Terms acceptance validation
  if (!formData.acceptedTerms) {
    return { valid: false, error: 'You must accept the terms' };
  }

  // All validations passed
  return { valid: true, data: formData };
}

Example 3: Order Processing

// BEFORE: Complex business logic in nesting
function processFoodOrder(order) {
  if (order && order.items && order.items.length > 0) {
    if (inventory.checkAvailability(order.items)) {
      const total = calculateTotal(order.items);
      if (order.payment) {
        if (order.payment.amount >= total) {
          if (order.deliveryLocation) {
            if (isDeliveryLocationSafe(order.deliveryLocation)) {
              if (deliveryPersonnel.isAvailable()) {
                const receipt = generateReceipt(order, total);
                inventory.reduceStock(order.items);
                scheduleDelivery(order);
                return { success: true, receipt };
              } else {
                return { success: false, reason: 'No available delivery personnel' };
              }
            } else {
              return { success: false, reason: 'Unsafe delivery location' };
            }
          } else {
            return { success: false, reason: 'Missing delivery location' };
          }
        } else {
          return { success: false, reason: 'Insufficient payment amount' };
        }
      } else {
        return { success: false, reason: 'Missing payment data' };
      }
    } else {
      return { success: false, reason: 'Products unavailable in stock' };
    }
  } else {
    return { success: false, reason: 'Invalid order' };
  }
}

// AFTER: Business logic divided into steps
function processFoodOrder(order) {
  // Step 1: Order validation
  const validationError = validateOrder(order);
  if (validationError) {
    return { success: false, reason: validationError };
  }

  // Step 2: Check stock availability
  if (!inventory.checkAvailability(order.items)) {
    return { success: false, reason: 'Products unavailable in stock' };
  }

  // Step 3: Payment verification
  const total = calculateTotal(order.items);
  const paymentError = validatePayment(order.payment, total);
  if (paymentError) {
    return { success: false, reason: paymentError };
  }

  // Step 4: Delivery verification
  const deliveryError = validateDelivery(order.deliveryLocation);
  if (deliveryError) {
    return { success: false, reason: deliveryError };
  }

  if (!deliveryPersonnel.isAvailable()) {
    return { success: false, reason: 'No available delivery personnel' };
  }

  // Step 5: Order fulfillment
  const receipt = generateReceipt(order, total);
  inventory.reduceStock(order.items);
  scheduleDelivery(order);

  return { success: true, receipt };
}

// Helper functions
function validateOrder(order) {
  if (!order) return 'Missing order';
  if (!order.items || order.items.length === 0) return 'No products in order';
  return null;
}

function validatePayment(payment, requiredAmount) {
  if (!payment) return 'Missing payment data';
  if (payment.amount < requiredAmount) return 'Insufficient payment amount';
  return null;
}

function validateDelivery(location) {
  if (!location) return 'Missing delivery location';
  if (!isDeliveryLocationSafe(location)) return 'Unsafe delivery location';
  return null;
}

Additional Techniques

1. Use Optional Chaining and Nullish Coalescing

// BEFORE: Nested null/undefined checking
function getDinosaurLocation(dinosaurId) {
  const dino = dinosaurs.get(dinosaurId);
  if (dino) {
    if (dino.enclosure) {
      if (dino.enclosure.location) {
        return dino.enclosure.location.coordinates;
      }
    }
  }
  return null;
}

// AFTER: Optional chaining
function getDinosaurLocation(dinosaurId) {
  const dino = dinosaurs.get(dinosaurId);
  return dino?.enclosure?.location?.coordinates ?? null;
}

2. Extract Complex Conditions into Variables

// BEFORE: Hard to understand condition
function shouldEvacuate(park) {
  if ((park.fenceStatus === 'breached' && park.dangerousDinosaurs > 0) ||
      (park.powerOutage && park.backupGeneratorFailed) ||
      (park.weatherAlert === 'severe' && park.visitorCount > 100)) {
    return true;
  }
  return false;
}

// AFTER: Named conditions
function shouldEvacuate(park) {
  const hasDangerousBreach = park.fenceStatus === 'breached' && park.dangerousDinosaurs > 0;
  const hasTotalPowerFailure = park.powerOutage && park.backupGeneratorFailed;
  const hasWeatherEmergency = park.weatherAlert === 'severe' && park.visitorCount > 100;

  return hasDangerousBreach || hasTotalPowerFailure || hasWeatherEmergency;
}

3. Use Polymorphism Strategy Instead of switch/if-else

// BEFORE: Long if-else chain
function handleDinosaurBehavior(dinosaur, behavior) {
  if (behavior === 'aggressive') {
    activateDefenses();
    alertSecurity();
    evacuateVisitors();
  } else if (behavior === 'scared') {
    reduceNoiseLevel();
    increaseDistance();
  } else if (behavior === 'hungry') {
    prepareFoodRation();
    scheduleFeedingTime();
  } else if (behavior === 'sleeping') {
    reduceActivity();
    dimLights();
  } else if (behavior === 'playful') {
    allowInteraction();
    monitorClosely();
  } else {
    logUnknownBehavior(behavior);
  }
}

// AFTER: Strategy with a function map
const behaviorHandlers = {
  aggressive: () => {
    activateDefenses();
    alertSecurity();
    evacuateVisitors();
  },
  scared: () => {
    reduceNoiseLevel();
    increaseDistance();
  },
  hungry: () => {
    prepareFoodRation();
    scheduleFeedingTime();
  },
  sleeping: () => {
    reduceActivity();
    dimLights();
  },
  playful: () => {
    allowInteraction();
    monitorClosely();
  }
};

function handleDinosaurBehavior(dinosaur, behavior) {
  const handler = behaviorHandlers[behavior];

  if (!handler) {
    logUnknownBehavior(behavior);
    return;
  }

  handler();
}

Summary

"Life... finds a way," says Dr. Malcolm. "But code shouldn't be a maze. Every function should have a clear, linear path - from validation at the beginning to business logic at the end."

Key principles:

  1. Check errors first - use early returns for edge cases
  2. Negate conditions - instead of if (x) use if (!x) return
  3. Avoid nesting - maximum 2-3 levels
  4. Extract logic - complex conditions -> helper functions
  5. Name conditions - assign complex expressions to variables
  6. Use modern JS - optional chaining, nullish coalescing

Code with early returns is:

  • More readable - easier to understand the flow
  • Easier to maintain - easy to add a new condition
  • Less error-prone - fewer places where things can go wrong
  • More testable - each condition can be tested separately

Remember: good code isn't just code that works - it's code that is easy to understand and modify. And in Jurassic Park, where every mistake can have catastrophic consequences, code readability can save lives!

Code for this lesson: index.ts
// Avoiding deep nesting - Early Returns
// Jurassic Park: A security system with guard clauses
console.log("=== EARLY RETURNS - JURASSIC PARK ===\n");

// ===========================================
// 1. Example: Checking a dinosaur's safety
// ===========================================
console.log("=== 1. SAFETY CHECK ===");

interface DinosaurData {
  name: string;
  isAlive: boolean;
  health: number;
  aggressionLevel: number;
  enclosure?: {
    fence?: {
      isActive: boolean;
      voltage: number;
    };
  };
  lastFed?: number;
}

// Early returns - each condition is checked separately
function checkSafety(dino: DinosaurData): string {
  if (!dino) {
    return "ERROR: No dinosaur data";
  }

  if (!dino.isAlive) {
    return "Dinosaur dead - no threat";
  }

  if (!dino.enclosure?.fence) {
    return "ALARM: No fence!";
  }

  if (!dino.enclosure.fence.isActive) {
    return "ALARM: Fence inactive!";
  }

  if (dino.health <= 30) {
    return "WARNING: Low health level";
  }

  if (dino.aggressionLevel >= 8) {
    return "ALARM: Very high aggression level!";
  }

  return "Status: Safe";
}

const trex: DinosaurData = {
  name: "T-Rex",
  isAlive: true,
  health: 85,
  aggressionLevel: 6,
  enclosure: { fence: { isActive: true, voltage: 10000 } },
  lastFed: Date.now() - 3600000
};

const brokenFence: DinosaurData = {
  name: "Velociraptor",
  isAlive: true,
  health: 90,
  aggressionLevel: 9,
  enclosure: { fence: { isActive: false, voltage: 0 } }
};

console.log(`${trex.name}: ${checkSafety(trex)}`);
console.log(`${brokenFence.name}: ${checkSafety(brokenFence)}`);

// ===========================================
// 2. Validation with early returns
// ===========================================
console.log("\n=== 2. FORM VALIDATION ===");

interface VisitorForm {
  firstName?: string;
  lastName?: string;
  age?: number;
  email?: string;
  acceptedTerms?: boolean;
}

function validateVisitor(form: VisitorForm): { valid: boolean; error?: string } {
  if (!form.firstName) {
    return { valid: false, error: "First name missing" };
  }

  if (!form.lastName) {
    return { valid: false, error: "Last name missing" };
  }

  if (!form.age || form.age < 5) {
    return { valid: false, error: "Minimum age: 5 years" };
  }

  if (!form.email || !form.email.includes("@")) {
    return { valid: false, error: "Invalid email" };
  }

  if (!form.acceptedTerms) {
    return { valid: false, error: "You must accept the terms" };
  }

  return { valid: true };
}

const goodForm: VisitorForm = {
  firstName: "John",
  lastName: "Smith",
  age: 25,
  email: "john@park.com",
  acceptedTerms: true
};

const badForm: VisitorForm = {
  firstName: "Anna",
  age: 3
};

console.log("Valid form:", validateVisitor(goodForm));
console.log("Invalid form:", validateVisitor(badForm));

// ===========================================
// 3. TODO: Try it yourself!
// ===========================================
console.log("\n=== 3. YOUR TURN ===");
console.log("Refactor the code below using early returns:");

// TODO: Rewrite this function using early returns
// instead of nested if-else

function processOrder(items: string[], payment: number) {
  // Use early returns instead of nested conditions!
  if (items && items.length > 0) {
    if (payment > 0) {
      if (payment >= items.length * 10) {
        console.log("Order processed!");
        return true;
      } else {
        console.log("Amount too small");
        return false;
      }
    } else {
      console.log("No payment");
      return false;
    }
  } else {
    console.log("No products");
    return false;
  }
}

processOrder(["VIP ticket", "Lunch"], 50);
processOrder([], 10);
processOrder(["Ticket"], 5);

console.log("\nHint: Negate the conditions and return early!");

Docs entries used in this lesson

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. What is the so-called "Arrow Anti-Pattern" (Pyramid of Doom) in code?

  2. 2. What does dino?.enclosure?.location ?? null return when dino is undefined?

Hands-on tasks in the game

  • Vertical ordering

    Order the elements in a function that uses early returns (top to bottom):

  • Click in order

    Arrange the elements of a guard clause:

Useful articles