Taming nested conditions
On This Lesson 11 topics
Lesson Overview
Real rules come with conditions inside conditions: "if the user is logged in, and if the account is active, and if the quantity is positive, then accept the order". Written naively, that becomes an if inside an if inside an if, each level pushing the code further to the right. By the third level nobody can say with confidence which road a given input takes.
This lesson teaches three tools to flatten such code: combining tests with && and ||, early exits that reject bad cases first, and naming a complicated test with a boolean variable. It also teaches you to audit a set of conditions for overlapping and unreachable branches, which is how many logic bugs hide.
It builds on all earlier lessons: if/else (L1), chains and boundaries (L2), and switch (L3 and L4), plus the logical operators from Module 10. The next lesson uses these skills to code a realistic shipping-cost rule.
Learning Objectives
By the end of this lesson you will be able to:
- Replace nested
ifstatements with&&where the structure allows it. - Rewrite a nested decision as a flat sequence using early exits (guard clauses).
- Name a complicated condition with a
booleanvariable to make it readable. - Identify an unreachable branch and an overlapping pair of conditions.
- Explain how an
elsepairs with anifwhen braces are missing. - Decide when nesting is justified and when it is a warning sign.
Written Explanation
Why deep nesting hurts
Each nesting level adds a fact you must hold in your head: "I am here only if A is true, and B is true, and...". Three levels deep, the actual action sits far from the conditions that lead to it, and the else branches are scattered at the bottom, far from the if they belong to. Beginners add a level for every new rule. Experienced developers ask how to remove one.
Tool 1: combine with && and ||
Two nested if statements with no else between them are the same as one if with &&:
// nested
if (a) {
if (b) {
run();
}
}
// equivalent
if (a && b) {
run();
}The first version runs run() only when a is true and, inside that, b is true. Both must hold, which is exactly &&. Because && is short-circuit, b is not even evaluated when a is false, so you can safely write x != 0 && 10 / x > 2.
Use || when several different conditions lead to the same action: if (isMember || hasCoupon). Mix them with parentheses, because && binds tighter than ||.
This tool works when all the cases lead to the same action. It does not help when each failed test needs its own message, as in the next tool.
Tool 2: early exits (guard clauses)
When each condition has a different failure message, deal with the failures first and leave the normal path at the end. This is called an early exit or guard clause: test for the bad case, report it, and leave.
In a method you leave with return. You have not met methods yet (they start in a later module), but main is a method, and a return; inside it simply ends the program. We use that here for the pattern; the same style carries over into every method you will write later.
The shape:
if (badCase1) { report; return; }
if (badCase2) { report; return; }
// all checks passed: normal work hereThe normal path sits at the lowest indentation, and every check is one line away from its message. Compare this with the nested version in Example 3.
Tool 3: name the condition
A long test is easier to read when each part has a name:
boolean isSenior = age >= 65;
boolean isChild = age < 12;
boolean discountEligible = member && (isSenior || isChild);
if (discountEligible) { ... }A reader now sees the rule, not the arithmetic. A named variable also gives you somewhere to put a comment and a place to print while debugging.
How else pairs when braces are missing
An else always belongs to the nearest unmatched if before it. With braces this is obvious; without them, your indentation can lie. Example 2 shows it. Another reason for the "always use braces" rule from Lesson 1.
Auditing conditions: unreachable and overlapping branches
When you finish a decision, check every pair of branches.
- Unreachable branch: a branch that can never run because earlier tests catch every value that could reach it. In
if (x > 5) {...} else if (x > 10) {...}, anyxabove 10 is already above 5, so the second block is dead code. - Overlap: two conditions that can be true for the same value. In a chain the earlier one wins silently; in separate
ifstatements both run. Either way, the author probably did not mean it. - Gap: a value that no branch handles (Lesson 2).
Java's compiler does not warn about these logic errors, even though the dead code is easy to see. (It does reject statements that are provably unreachable in specific structural ways, such as code after return, but not an if branch whose condition happens to be impossible.) The only defence is to test: for each branch, find an input that reaches it. If you cannot find one, the branch is dead.
Examples
Example 1: && instead of nesting (simple)
boolean a = true, b = true;
if (a) {
if (b) {
System.out.println("nested");
}
}
if (a && b) {
System.out.println("combined");
}Output:
nested
combinedBoth forms behave the same. The second is shorter and says in one line what must hold.
Example 2: when indentation lies (practical)
int p = 5;
if (p > 0)
if (p > 10)
System.out.println("big");
else
System.out.println("not positive?");
System.out.println("end");Real output:
not positive?
endThe author's layout suggests that the else belongs to p > 0. It does not: it pairs with the nearest if, p > 10. Since 5 is positive but not above 10, the program prints not positive?, which is nonsense for a positive number. With braces and correct indentation the intent becomes explicit:
if (p > 0) {
if (p > 10) {
System.out.println("big");
}
} else {
System.out.println("not positive");
}Example 3: validating an order, nested versus flat (realistic)
The rules: the customer must be logged in, the account must be active, and the quantity must be positive. Each failure has its own message.
Nested version:
if (loggedIn) {
if (active) {
if (qty > 0) {
System.out.println("Order accepted");
} else {
System.out.println("Bad quantity");
}
} else {
System.out.println("Account inactive");
}
} else {
System.out.println("Please log in");
}Flat version with early exits:
if (!loggedIn) {
System.out.println("Please log in");
return;
}
if (!active) {
System.out.println("Account inactive");
return;
}
if (qty <= 0) {
System.out.println("Bad quantity");
return;
}
System.out.println("Order accepted");I ran both versions in one program. With loggedIn and active true and qty = 3, each printed Order accepted. With qty = 0, each printed Bad quantity (and the flat version's return ended the program). They agree on every input, but in the flat version each message sits next to its test and the normal path is the last line.
The flat version has one thing to watch: the tests are written as the opposite of the success condition (!loggedIn, qty <= 0). Check each opposite carefully; the complement of qty > 0 is qty <= 0, not qty < 0.
Backend Insight
Request handling code in a backend almost always starts with a block of guard clauses: is the user authenticated, is the user allowed, is the input valid, does the thing exist? Only then does the real work happen. This shape is so common that reviewers expect it, and deep nesting in such code is read as a warning: someone probably added rules on top of rules without restructuring.
A second reason to flatten is testing. Each guard is one rule with one message, so each can be tested with one input. A five-level nest has combinations that nobody will test. And a third reason is change: when the product team adds a new rule ("blocked accounts cannot order"), a flat structure takes one more guard, while a nest takes a new level and rewrites the else branches around it.
Common Mistakes
Mistake 1: Nesting by reflex
- Incorrect: a new
iflevel for every rule, three or more levels deep. - Correct:
&&for rules with the same outcome, guard clauses for rules with their own messages. As a rough guide, if you reach three levels of indentation, stop and flatten.
Mistake 2: Wrongly negating a condition
- Incorrect: the opposite of
qty > 0written asqty < 0. The value 0 then gets through. - Correct:
qty <= 0. Fora && b, the opposite is!a || !b(De Morgan's laws), not!a && !b.
When in doubt, test with the boundary value, here 0.
Mistake 3: Trusting indentation for else
See Example 2. Use braces and let a formatter or IDE re-indent your code; if the layout changes, you have found a misleading structure.
Mistake 4: Leaving an unreachable branch
- Incorrect:
if (age >= 0) {...} else if (age > 100) {...}. Values above 100 never reach the second test. - Correct: test the narrower condition first, or remove the dead branch.
Mistake 5: Overusing a boolean name
boolean flag = a && b; explains nothing. Name the idea (canCheckout), not the mechanism.
Exercises
Level A: Foundation
A1. Rewrite as one if:
if (loggedIn) {
if (isAdmin) {
showPanel();
}
}Show solution A1
A1. if (loggedIn && isAdmin) { showPanel(); }
A2. Which branch is unreachable? Explain.
if (n >= 0) { System.out.println("non-negative"); }
else if (n > 100) { System.out.println("large"); }
else { System.out.println("negative"); }Show solution A2
A2. n > 100 is unreachable: every n above 100 is already non-negative, so the first branch catches it. The else is reached only by negative values. Fix: test n > 100 first, or drop that branch.
A3. What is the opposite (complement) of price > 0 && stock > 0?
Show solution A3
A3. price <= 0 || stock <= 0. By De Morgan's laws, the negation of an && is an || of the negated parts.
Level B: Application
B1. Predict the output without running it, then check by running:
int n = 4;
if (n > 0)
if (n > 10)
System.out.println("A");
else
System.out.println("B");Show solution B1
B1. B. The else pairs with the nearest if, n > 10. With n = 4, the outer test is true, the inner false, so the else runs. The layout suggests otherwise, which is why braces matter.
B2. Rewrite this nested code with early exits. Use System.out.println for each message and return; after it:
if (age >= 18) {
if (idChecked) {
System.out.println("Entry allowed");
} else {
System.out.println("Show ID");
}
} else {
System.out.println("Too young");
}Show solution B2
B2.
if (age < 18) {
System.out.println("Too young");
return;
}
if (!idChecked) {
System.out.println("Show ID");
return;
}
System.out.println("Entry allowed");B3. Define boolean variables to simplify if (day == 6 || day == 7 || holiday) { ... } and explain what you gained.
Show solution B3
B3.
boolean weekend = day == 6 || day == 7;
if (weekend || holiday) { ... }The condition now reads like the rule. You also have a named piece you can reuse in other conditions and print when debugging.
Level C: Challenge
C1. A discount rule: members get 10 percent. Non-members get 5 percent if they buy 5 or more items. Others get none. Write it as a flat chain with a double discount. Then list the four combinations of member and quantity (above and below 5) and the expected result of each.
Show solution C1
C1.
double discount;
if (member) {
discount = 0.10;
} else if (qty >= 5) {
discount = 0.05;
} else {
discount = 0;
}Expected: member with any quantity gives 0.10, regardless of count; non-member with 5 or more gives 0.05; non-member with fewer than 5 gives 0. The third row demonstrates that a flat chain needs no member && qty < 5 in the last branch, because earlier tests already filtered members.
C2. A teammate wrote:
if (temp > 30) { status = "hot"; }
if (temp > 20) { status = "warm"; }
if (temp > 10) { status = "mild"; }
else { status = "cold"; }State what status 35 gets and why. Then fix the code. Which concept from this lesson is involved?
Show solution C2
C2. For 35, the first if sets hot, then the second sets warm, then the third sets mild: the last assignment wins, so 35 ends as mild. These are separate if statements, so overlapping conditions all run, and only the last else belongs to the last if. The fix is an else if chain: if (temp > 30) ... else if (temp > 20) ... else if (temp > 10) ... else .... Concepts: overlap and chain order.
Mini Challenge
A conference app accepts a registration only when all of these hold: a boolean nameGiven is true, the age is at least 16, the ticket count is between 1 and 4 inclusive, and, if the ticket count is 3 or more, the attendee agreed to the group terms (agreedTerms). Each failure needs its own message.
- Write the flat version with early exits and one
printlnper failure. - Write a table with at least 8 test inputs, including every boundary (age 15 and 16, tickets 0, 1, 4, 5, and 2 versus 3 with and without the terms).
- Predict each outcome by hand, then run.
Check your work: no nesting deeper than one level, and each guard is tested by at least one input that triggers it.
Summary & Next Lesson
Key takeaways
- Nested
ifwith noelsebetween them equals oneifwith&&. - Guard clauses (early exits) handle each failure first and leave the normal path flat.
- Name complicated conditions with
booleanvariables. - An
elsepairs with the nearestif; braces make the pairing visible. - Audit every pair of branches for unreachable code, overlaps, and gaps; the compiler will not do it for you.
Next lesson
Time to put it all together. The next lesson, "Demo: shipping cost rules and their boundaries", turns written shipping rules into clean conditions and builds a test table around every boundary, preparing you for the shipping lab.
Self-check
- Which operator replaces an
ifinside anifwith noelse? - What does
return;do insidemain? - What is wrong with writing the opposite of
a && bas!a && !b? - In
if (x > 5) {...} else if (x > 10) {...}, when does the second block run? - To which
ifdoes anelsebelong?
Show answers
&&.- It ends the
mainmethod, so the program ends. - The correct opposite is
!a || !b; the wrong version is true only when both are false. - Never; any
xabove 10 is already above 5. - The nearest unmatched
ifbefore it, regardless of indentation.
References
- Oracle, The Java Language Specification, Java SE 21, Chapter 14. Supports the rule that an
elsebelongs to the innermostifit can pair with (the "dangling else" rule). - Outputs were produced by compiling and running the examples with JDK 21. The claim that the compiler does not flag an impossible
else ifcondition was confirmed by compiling such code without errors; the exact unreachable-statement rules (JLS section 14.22) were not read in full, so that claim is limited to what I observed.
Practice
Apply this lesson with the module's lab, exercises and quiz.
Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.