Lesson 25 of 54

Control Flow and Logic

Avoiding Deep Nesting

Every level of nesting is a cognitive burden.

When you read nested code, your brain maintains a mental stack: "I'm inside this if, which is inside this loop, which is inside this try block..." By the time you're three or four levels deep, you've exhausted working memory.

The research is clear: deeply nested code has more bugs, takes longer to understand, and is harder to modify safely.

The Mental Stack Problem

Consider this code:

Javascript
function processItems(items) {
  if (items) {
    for (const item of items) {
      if (item.active) {
        if (item.quantity > 0) {
          if (item.price) {
            for (const discount of applicableDiscounts) {
              if (discount.applies(item)) {
                // Finally, the actual logic
                // What context are we in again?
              }
            }
          }
        }
      }
    }
  }
}

To understand the innermost code, you need to remember:

  1. Items exist
  2. We're iterating over items
  3. The current item is active
  4. The current item has quantity > 0
  5. The current item has a price
  6. We're iterating over discounts
  7. The current discount applies to the item

That's 7 items in your mental stack. Most people can hold about 4.

Maximum Nesting Depth

Rule of thumb: maximum 2-3 levels of nesting.

Beyond that, your code is telling you something needs to be extracted or restructured.

Technique 1: Guard Clauses

We covered this in the previous lesson, but it bears repeating.

Before

Javascript
function processOrder(order) {
  if (order) {
    if (order.isValid) {
      if (order.isPaid) {
        fulfill(order);
      }
    }
  }
}

After

Javascript
function processOrder(order) {
  if (!order) return;
  if (!order.isValid) return;
  if (!order.isPaid) return;
  
  fulfill(order);
}

Technique 2: Extract Functions

When nested code represents a distinct operation, extract it.

Before

Javascript
function processOrders(orders) {
  const results = [];
  for (const order of orders) {
    if (order.status === 'pending') {
      if (order.hasInventory) {
        const total = 0;
        for (const item of order.items) {
          if (item.inStock) {
            total += item.price * item.quantity;
          }
        }
        if (total > 0) {
          results.push({ orderId: order.id, total });
        }
      }
    }
  }
  return results;
}

After

Javascript
function processOrders(orders) {
  return orders
    .filter(isPendingWithInventory)
    .map(calculateOrderTotal)
    .filter(result => result.total > 0);
}

function isPendingWithInventory(order) {
  return order.status === 'pending' && order.hasInventory;
}

function calculateOrderTotal(order) {
  const total = order.items
    .filter(item => item.inStock)
    .reduce((sum, item) => sum + item.price * item.quantity, 0);
  
  return { orderId: order.id, total };
}

Each function is flat. The logic is composed.

Technique 3: Use Array Methods

Loops often nest unnecessarily. Functional array methods flatten them.

Before: Nested Loops

Javascript
const results = [];
for (const category of categories) {
  for (const product of category.products) {
    if (product.inStock) {
      results.push(product.name);
    }
  }
}

After: Flat Chain

Javascript
const results = categories
  .flatMap(category => category.products)
  .filter(product => product.inStock)
  .map(product => product.name);

Technique 4: Invert Conditions

Sometimes inverting a condition lets you return early.

Before

Javascript
function handleResponse(response) {
  if (response.ok) {
    const data = parseData(response.body);
    if (data.valid) {
      return processData(data);
    } else {
      log('Invalid data');
    }
  } else {
    throw new ResponseError(response.status);
  }
}

After

Javascript
function handleResponse(response) {
  if (!response.ok) {
    throw new ResponseError(response.status);
  }
  
  const data = parseData(response.body);
  
  if (!data.valid) {
    log('Invalid data');
    return;
  }
  
  return processData(data);
}

Technique 5: Use continue in Loops

continue skips to the next iteration, reducing nesting inside loops.

Before

Javascript
for (const user of users) {
  if (user.isActive) {
    if (user.hasSubscription) {
      if (!user.isBlocked) {
        sendNewsletter(user);
      }
    }
  }
}

After

Javascript
for (const user of users) {
  if (!user.isActive) continue;
  if (!user.hasSubscription) continue;
  if (user.isBlocked) continue;
  
  sendNewsletter(user);
}

Technique 6: Use Early break

For search loops, break early when you find what you need.

Before

Javascript
function findFirstActive(users) {
  let result = null;
  for (const user of users) {
    if (user.isActive) {
      if (result === null) {
        result = user;
      }
    }
  }
  return result;
}

After

Javascript
function findFirstActive(users) {
  for (const user of users) {
    if (user.isActive) {
      return user;
    }
  }
  return null;
}

Or just:

Javascript
function findFirstActive(users) {
  return users.find(user => user.isActive) ?? null;
}

Technique 7: Replace Nested Callbacks

Callback hell is a special form of nesting.

Before: Callback Pyramid

Javascript
getUser(userId, (err, user) => {
  if (err) return handleError(err);
  getOrders(user.id, (err, orders) => {
    if (err) return handleError(err);
    getShipments(orders, (err, shipments) => {
      if (err) return handleError(err);
      render(user, orders, shipments);
    });
  });
});

After: Async/Await

Javascript
async function loadDashboard(userId) {
  const user = await getUser(userId);
  const orders = await getOrders(user.id);
  const shipments = await getShipments(orders);
  render(user, orders, shipments);
}

Measuring Nesting

Tools can measure cyclomatic complexity and nesting depth:

  • ESLint's max-depth rule
  • SonarQube's cognitive complexity
  • Code Climate metrics

Configure them:

Json
{
  "rules": {
    "max-depth": ["error", 3]
  }
}

Now deeply nested code fails linting.


Key insight: Every nesting level is a mental burden. Use guard clauses, extract functions, leverage array methods, and invert conditions to keep code flat. When you can't read the code without losing track of context, it's too nested.