Unit 2 · Lesson 4

Conditional Statements

A robot with no conditionals can only do one thing. Conditionals are the decision layer — they let the robot ask "what state am I in?" and "what should I do right now?" Every intake toggle, every arm position limit, every autonomous mode selection runs through this logic fifty times a second.

By the end of this lesson, you will:

  • Write correct if, if-else, and if-else if-else chains for multi-condition robot decisions
  • Explain why the order of conditions in a chain encodes a priority hierarchy
  • Use switch for mode and state selection, including the break keyword and fall-through behavior
  • Apply the ternary operator for concise, single-value conditional assignments
  • Refactor deeply nested conditions into guard clauses for flatter, more readable robot code
  • Identify the three most common conditional bugs: missing curly braces, = vs ==, and unguarded fall-through

Decisions in a 20ms Loop

Every 20 milliseconds, the periodic method runs from top to bottom. There are no pauses, no waiting — just code executing in sequence, fifty times a second. Conditional statements are how you encode the robot's decision logic into that sequence: given the current state of every input right now, what should each mechanism do?

That framing matters. In robot code, a conditional isn't "should I bring an umbrella?" — it's "given that the driver is holding the right bumper, the arm is above 45 degrees, and the beam break is clear, should the shooter wheel be spinning?" The conditions are physical. Getting the logic wrong moves hardware in the wrong direction.

The if — else if — else Ladder

Java evaluates an if-else if-else chain from top to bottom. The first condition that evaluates to true runs its block and the rest of the chain is skipped entirely. This makes ordering a design decision, not just a formatting choice — the most specific or highest-priority conditions go first.

The decision tree below models a real arm controller. Click any branch to trace which block runs and understand why its position in the chain matters.

Arm Controller Decision Tree — click a branch to trace execution
if hasFault() || currentAmps > MAX_AMPS
else if eStopPressed
else if scoreButton && armAtSafeAngle()
else if intakeButton && !gamePieceHeld
else no other condition matched
← click any branch to see what executes and why that position in the chain matters
// The arm controller — order encodes a real priority hierarchy public void updateArm() { if (hasFault() || currentAmps > MAX_AMPS) { armMotor.set(0.0); // 1. Hardware safety — always wins logFault("arm cutoff"); } else if (eStopPressed) { armMotor.set(0.0); // 2. Driver override — beats game logic } else if (scoreButton && armAtSafeAngle()) { setArmTarget(SCORE_ANGLE_DEG); // 3. Active driver command } else if (intakeButton && !gamePieceHeld) { setArmTarget(INTAKE_ANGLE_DEG); // 4. Another driver command } else { setArmTarget(STOW_ANGLE_DEG); // 5. Safe default when nothing else applies } }

Swapping the fault check and the score button means a faulted motor keeps moving as long as the driver holds a button. That isn't a code style preference — it's a safety decision written in ordering.

The Missing Curly Braces Bug

Java allows a single-statement if body without curly braces. This is one of the most reliably problematic features of the language. Without braces, only the very next statement belongs to the if. Every statement after it runs unconditionally — even if the indentation implies otherwise.

// ❌ Looks like both statements are conditional — only the first one is if (currentAmps > MAX_AMPS) armMotor.set(0.0); // only this is conditional logFault("overcurrent"); // ALWAYS runs — indentation is a lie // The compiler sees it as: if (currentAmps > MAX_AMPS) { armMotor.set(0.0); } logFault("overcurrent"); // unconditional — logs on every loop cycle // ✅ Always use curly braces — no exceptions, no debate if (currentAmps > MAX_AMPS) { armMotor.set(0.0); logFault("overcurrent"); }
🔍 LRI Observation

The missing-braces bug is invisible in a text editor because indentation makes it look conditional. On a robot, the consequence I see most often is runaway logging: a "fault detected" message floods the console on every 20ms cycle regardless of whether there's an actual fault, and real faults become impossible to spot in the noise. Team 2910's style rule is absolute: curly braces on every block, always. Some teams configure their linter to enforce it automatically so nobody has to think about it.

The switch Statement

When you're selecting behavior based on a single variable matching one of several specific values — a mode integer, a state enum, a string label — switch is often cleaner than a long else if ladder. It jumps directly to the matching case instead of evaluating conditions from top to bottom.

The detail that trips people up every season: the break keyword is mandatory unless you specifically intend fall-through. Without it, execution continues into the next case's code automatically, with no warning from the compiler.

Each case ends with break, so execution exits the switch block after the matching case runs. This is the pattern you want the vast majority of the time.

int autoMode = 1; switch (autoMode) { case 0: runTaxiAuto(); break; // exits here — cases 1 and 2 do not run case 1: runOnePieceAuto(); break; // exits here — only this case ran case 2: runThreePieceAuto(); break; default: runTaxiAuto(); // safe fallback for unexpected values break; }

The default case is your safety net. A driver could accidentally select an out-of-range option on the dashboard. Without a default, nothing happens — the robot sits still at the start of autonomous. With a default that runs the simplest, safest auto routine, the robot at least moves.

Without break, execution falls through into the next case — running its code even though the value didn't match that case. The compiler does not warn you. The robot just does three things at once.

int autoMode = 0; switch (autoMode) { case 0: runTaxiAuto(); // ← runs (autoMode matches 0) // missing break! case 1: runOnePieceAuto(); // ← ALSO runs (fell through from case 0) // missing break! case 2: runThreePieceAuto(); // ← ALSO runs (fell through from case 1) break; default: runTaxiAuto(); break; }

All three auto routines attempt to run in sequence. Depending on what they do, this could initialize conflicting hardware states, send contradictory motor commands, or cause a loop overrun. The robot doesn't throw an error — it just behaves inexplicably until you think to search for a missing break.

Fall-through is occasionally intentional — when multiple cases should trigger identical behavior. The distinction from the bug: intentional fall-through has no code between the stacked cases, making the intent visually unambiguous.

// Intentional: LOW and MID both use the same safe arm extension switch (scoreTarget) { case LOW: case MID: // No code between cases — intentional fall-through is visually obvious extendArmToLow(); break; case HIGH: extendArmToHigh(); break; default: stowArm(); break; } // Rule: if fall-through is intentional, leave a comment saying so. // An unexplained missing break always looks like a bug to the next reader.
💡 switch vs. if-else: when to choose which

Use switch when you're comparing one variable against multiple constant values — a mode integer, a state enum, a string label from a chooser. Use if-else if when conditions involve ranges, multiple variables, or complex expressions like currentAmps > MAX_AMPS || hasFault(). A switch can only match equality. In Unit 5 you'll build state machines almost entirely on switch statements with enum values — that's the natural home of this construct.

The Ternary Operator

The ternary operator (? :) is a compact form of if-else that produces a value. It's not a conditional block — it's an expression that evaluates to one of two options based on a condition. Use it for simple single-value assignments where both branches fit cleanly on one line.

// Syntax: result = (condition) ? valueIfTrue : valueIfFalse; // Apply a speed cap when the driver activates precise mode double speedLimit = preciseMode ? 0.4 : 1.0; driveMotor.set(joystickInput * speedLimit); // Shuffle board state label — keep the display informative String intakeLabel = intakeRunning ? "INTAKING" : "IDLE"; SmartDashboard.putString("Intake", intakeLabel);

When not to use it

The ternary is a one-condition, one-value tool. Nesting ternaries to handle three or more cases produces code that nobody can parse under pressure:

// ❌ Nested ternary — takes 30 seconds to parse, 3 seconds to break double out = fault ? 0.0 : (precise ? 0.3 : (boost ? 1.0 : 0.7)); // ✅ if-else if — longer, but readable at 9 AM in a pit with a referee watching double out; if (fault) out = 0.0; else if (precise) out = 0.3; else if (boost) out = 1.0; else out = 0.7;

Guard Clauses: Keeping Conditions Readable

Deeply nested if statements are common in robot code and make it hard to trace what actually runs under which conditions. The fix is the guard clause: check for failure cases first with an early return, then let the main logic run at the top level of indentation.

// ❌ Arrow of doom — the action is buried 4 levels deep public void runShooter() { if (!hasFault()) { if (armInRange()) { if (shootButton) { if (intakeClear) { shooterMotor.set(TARGET_SPEED); // found it } } } } }
// ✅ Guard clauses — fail fast, keep the success path flat public void runShooter() { if (hasFault()) { shooterMotor.set(0.0); return; } if (!armInRange()) { shooterMotor.set(0.0); return; } if (!shootButton) { shooterMotor.set(0.0); return; } if (!intakeClear) { shooterMotor.set(0.0); return; } // All preconditions met shooterMotor.set(TARGET_SPEED); }

Both versions behave identically. The guard clause version has four explicit preconditions, each with a safe fallback action. Adding a new precondition next week — a "gyro calibrated" check, a "battery above 11V" check — slots in as one more guard clause without restructuring the whole method.

🔍 Event Observation

At a regional, I watched a team spend half of their lunch break debugging a shooter that wouldn't fire on certain driver inputs. Their method had five levels of nesting. The bug was a missing ! on a boolean buried in the third level — a condition that was supposed to check "is the intake not running" but instead checked "is the intake running." The guard-clause refactor took about ten minutes and made the missing inversion immediately obvious. Shallow code is a debugging speed multiplier, and debugging speed matters most when you have the least time.

Revisiting = vs. == in a Conditional

Lesson 3 introduced this as an operator distinction. Here it is in the conditional context where it actually causes robot bugs. It's worth repeating because it appears at every experience level, and with a boolean on the left side, the Java compiler's warning is easily missed.

// ❌ Assignment in condition — always evaluates to true if (intakeRunning = true) { // assigns true, then tests true — runs every cycle intakeMotor.set(INTAKE_SPEED); // motor never stops regardless of actual state } // ✅ Comparison if (intakeRunning == true) { intakeMotor.set(INTAKE_SPEED); } // ✅✅ Preferred — the boolean IS the condition if (intakeRunning) { intakeMotor.set(INTAKE_SPEED); } // ✅✅ Preferred negation — !variable, not variable == false if (!intakeRunning) { intakeMotor.set(0.0); }

The best prevention is the last pattern: when a variable is boolean, use it directly as the condition. if (intakeRunning) can never be confused with an assignment. if (intakeRunning == true) opens the door for the one-character typo that changes the whole meaning.

🔌 System Check

⚙️ Conditional Logic and Physical Safety

Each of these has caused a real robot to do the wrong thing at a real competition.

  • Safety conditions must be first in every chain. Any condition that cuts motor power on a fault or overcurrent event must be the first if, not an else if buried after game logic. If a fault check is downstream of a driver command, an active driver input can prevent the safety from triggering.
  • Every if block gets curly braces — always. No exceptions for "simple" single-statement bodies. Lint rules can enforce this automatically; configure them at the start of the season and don't disable them.
  • Every switch case gets a break — or a comment explaining why it doesn't. An unexplained missing break is always treated as a bug during code review. If fall-through is intentional, document it.
  • Every switch gets a default case. Hardware selection on a dashboard, auto chooser, or mode selector can produce unexpected values. A default that runs your safest fallback behavior is the difference between "wrong auto routine" and "robot sits still in autonomous."
  • Boolean conditions should use the variable directly. if (limitSwitch), not if (limitSwitch == true). The shorter form eliminates the assignment-typo class of bug entirely.
  • Flatten nested conditions before code review. If any method in your robot code has more than three levels of indentation, it's a guard clause refactor waiting to happen. Deeply nested safety logic is harder to audit and easier to break on maintenance.

Knowledge Check

Click an answer to check your understanding.

An arm controller checks four conditions: fault, e-stop, score button, and a stow default. A new programmer puts the score button check first in the chain, before the fault check. What is the consequence?
  • 1No consequence — if-else chains check all conditions regardless of order
  • 2The code won't compile — safety conditions must be declared before game logic
  • 3While the driver holds the score button, the fault condition is never evaluated — the arm continues moving even during an overcurrent event, because the first matching branch runs and skips the rest
  • 4The score button will stop working because the fault check takes priority at runtime
A student forgets break in case 1 of a switch on autoMode. The robot is set to autoMode = 1 before the match. What runs during autonomous?
  • 1Nothing — a missing break causes the switch to skip all remaining cases
  • 2Only case 1 runs — Java skips cases without explicit matching
  • 3Case 1 runs, then execution falls through and case 2 also runs — the robot attempts both auto routines in sequence
  • 4The compiler catches the missing break and refuses to build
You have a method with four nested if statements that eventually calls motor.set(speed). A teammate refactors it using guard clauses. Which statement correctly describes what changes?
  • 1The behavior changes — guard clauses check conditions in reverse order
  • 2The method now runs faster because guard clauses skip evaluation of later conditions
  • 3The behavior is identical — but each failure case now has an explicit safe action and an early return, and the success path (motor.set(speed)) runs at the top level of indentation rather than four levels deep
  • 4Guard clauses are only valid in methods that return void
💪 Practice Prompt

Rewrite and Reason

The following method works — sort of. It controls a climber mechanism but has four distinct conditional problems. Find them, explain what could go wrong on the robot for each one, and produce a corrected version.

// This method has four conditional problems — find them all public void updateClimber() { if (climbButton && !hasFault()) climberMotor.set(CLIMB_SPEED); logClimbAttempt(); if (retractButton) climberMotor.set(-CLIMB_SPEED); if (hasFault()) climberMotor.set(0.0); if (climbButton = true && currentAmps > STALL_LIMIT) triggerFault(); }
  1. Identify all four problems. For each one, write one sentence describing what it does to the robot's physical behavior — not just what the syntax error is.
  2. Rewrite the method using guard clauses, corrected syntax, and curly braces on every block. Add a comment to each guard clause explaining what physical condition it is protecting against.
  3. The current method uses four separate if statements rather than an if-else if chain. Explain why this is a problem: what happens if climbButton is held and hasFault() returns true at the same time? How does your rewrite prevent this?
  4. Bonus: Add a switch statement to handle three climber modes — EXTEND, RETRACT, and HOLD — selected from an enum. The HOLD case should share the same behavior as a future BRAKE case using intentional fall-through. Add a comment documenting the fall-through intent.