Unit 2 · Lesson 5

Loops

Your robot already runs inside a loop — WPILib calls periodic methods fifty times per second, forever. Understanding Java loops means understanding how to do repetitive work correctly inside that constraint, and how to recognize when a loop will break the constraint entirely.

By the end of this lesson, you will:

  • Write correct while, for, do-while, and enhanced for-each loops with appropriate use cases for each
  • Explain why a blocking while loop inside a periodic method locks up the robot and what WPILib does in response
  • Identify where loops do belong in FRC code — initialization, data processing, iterating over hardware collections
  • Use break and continue to control loop execution for early-exit and filter patterns
  • Trace through a for loop step by step — initialization, condition, body, update — and predict the output at any point

The Loop You Didn't Write

Before covering Java's loop keywords, here's the most important loop in your robot program — the one WPILib writes for you:

// Inside TimedRobot — simplified. You never write this; WPILib runs it. while (robot.isEnabled()) { robot.teleopPeriodic(); // your code Thread.sleep(20); // wait for next 20ms tick }

Every periodic method you write is a loop body that executes on a 20ms timer. WPILib's watchdog monitors each iteration: if your code takes longer than 20ms to complete, the Driver Station logs a loop overrun warning. If the overrun is severe enough, the robot's communication with the field management system can drop, and the robot disables itself.

This is the context for every loop you write in robot code. A loop that runs for half a second in a desktop Java program is annoying. The same loop inside teleopPeriodic() disables your robot mid-match.

The Four Loop Patterns

Java has three loop keywords — while, for, and do-while — plus an enhanced for-each syntax for collections. Click each one to see its structure, when to reach for it, and where it appears in real FRC code.

while condition-first
for counted iteration
do-while body-first
for-each collection walk
loading...

Tracing a for Loop Step by Step

A for loop has four distinct phases that execute in a specific order. Understanding that order is what lets you predict exactly what any loop does without running it — which matters when you're reading team code or debugging a loop that's off by one. Step through the tracer below one phase at a time.

for Loop Tracer — applying current limits to four swerve modules
1for (int i = 0; i < modules.length; i++) {
2    modules[i].setCurrentLimit(LIMIT);
3}
4// modules.length = 4
Variables
i
condition
applied to
READY Click Step to begin tracing the loop.

The Most Dangerous Loop Pattern in FRC

Now for the thing that causes more match losses from software than almost any other mistake: a while loop placed inside a periodic method that waits for a hardware condition.

⚠️ Never put a blocking loop inside a periodic method

A blocking while loop inside teleopPeriodic() or autonomousPeriodic() prevents the periodic method from returning. WPILib cannot call the next iteration. The robot stops responding to the Driver Station. After a timeout, it disables. On a real competition field, the robot goes limp mid-match.

The watchdog can't save you from this — the loop isn't just slow, it's infinite from the watchdog's perspective until the hardware condition is met. By then, the match may be over.

// ❌ Blocking loop — robot freezes until the intake detects a game piece @Override public void teleopPeriodic() { if (intakeButton) { intakeMotor.set(0.6); while (!beamBreak.get()) { // blocks until sensor triggers — could be seconds // waiting... watchdog fires, DS loses comms, robot disables } intakeMotor.set(0.0); } }
// ✅ State-based — checks the sensor condition on each periodic call instead private boolean intakeActive = false; @Override public void teleopPeriodic() { if (intakeButton) { intakeActive = true; } if (intakeActive) { if (beamBreak.get()) { intakeMotor.set(0.0); // game piece detected — stop this cycle intakeActive = false; } else { intakeMotor.set(0.6); // still waiting — run motor and return } } // periodic() returns every 20ms regardless — watchdog stays happy }

The state-based version does the same thing. The intake runs until the beam break triggers. But instead of blocking and waiting, it checks the sensor on each periodic call and returns. The WPILib loop keeps running, the Driver Station keeps getting heartbeats, and the robot stays enabled.

🔍 Event Observation

In my second season as an LRI, I watched a team's robot go limp at the start of an autonomous routine. The robot had been working perfectly in the pit. On the field, a particular autonomous path started with a while (!encoderAtTarget()) loop waiting for the drive to complete a turn. In the pit, the turn took about 0.4 seconds — fast enough that the watchdog just barely tolerated it. On the field, something in the drivetrain was slightly off; the turn took 1.1 seconds. The watchdog fired, comms dropped, and the robot sat still for the entire autonomous period. The team lost the match by four points. The fix — converting the blocking loop into a state check — took eleven lines of code and about twenty minutes. They didn't get those three auto points back.

Where Loops Do Belong in Robot Code

The no-blocking-loops rule applies specifically to periodic methods. Loops are entirely appropriate in other parts of your robot code.

robotInit() — one-time setup

Configuring all four swerve modules at startup, applying current limits to every motor in an array, validating that all hardware objects are non-null — this is initialization work that runs once when the program starts. A for loop here is fine and common.

// robotInit() — configure every module before the match starts @Override public void robotInit() { for (int i = 0; i < modules.length; i++) { modules[i].setCurrentLimit(DRIVE_CURRENT_LIMIT); modules[i].setNeutralMode(NeutralModeValue.Brake); modules[i].burnFlash(); // save config to motor controller flash memory } System.out.println("All modules configured."); }

Utility and helper methods

Searching an array for a fault, averaging a set of sensor readings, finding the maximum current among all drive motors — these are data-processing tasks that a method can perform and return a result from. The method is called from periodic code, but the loop completes in microseconds.

// Helper method — loops over motors and returns the highest current reading private double getMaxDriveCurrent() { double maxAmps = 0.0; for (TalonFX motor : driveMotors) { // for-each — clean when order doesn't matter double amps = motor.getStatorCurrent().getValueAsDouble(); if (amps > maxAmps) { maxAmps = amps; } } return maxAmps; } // Called from periodic — the loop runs, completes, and returns in microseconds @Override public void teleopPeriodic() { if (getMaxDriveCurrent() > FAULT_THRESHOLD) { triggerFault(); } }

Autonomous command sequences

In Unit 6, Command-Based programming gives you tools for sequencing actions without blocking loops. But in a simple TimedRobot autonomous, a for loop iterating over a list of waypoints is acceptable if each waypoint executes synchronously and the whole sequence is fast enough. This pattern doesn't scale to complex autos — it's the reason elite teams move to Command-Based.

Loop Control: break and continue

Two keywords let you alter the normal flow of a loop from inside its body.

break — exit immediately

break terminates the loop entirely, jumping to the first statement after the closing brace. Use it to stop searching once you've found what you need — iterating past the answer wastes time and can cause unintended side effects.

// Search through CAN bus devices — stop as soon as a fault is found boolean faultDetected = false; for (TalonFX motor : allMotors) { if (motor.getFault_Hardware().getValue()) { faultDetected = true; logFault(motor.getDeviceID()); break; // found one — no need to check the rest this cycle } } // Execution continues here after break if (faultDetected) { safeState(); }

continue — skip to next iteration

continue skips the rest of the current iteration's body and jumps to the loop's update step (for a for loop) or back to the condition check (for while). Use it to filter out elements that don't need processing.

// Apply a command only to modules that are not in a fault state for (SwerveModule module : modules) { if (module.hasFault()) { continue; // skip this module — don't send it any commands } module.setDesiredState(desiredStates[module.getIndex()]); }
💡 break and continue in the periodic context

break and continue apply to the innermost loop they appear in — not to the periodic method itself. You can't use break to exit teleopPeriodic(). To exit a periodic method early, use return (same guard clause pattern from Lesson 4). Inside a loop within periodic code, both keywords are safe as long as the loop itself completes in well under 20ms.

The Three Loop Bugs That Appear Every Season

Bug 1: Off-by-one error

The most common loop mistake. Arrays in Java are zero-indexed — a four-element array has valid indices 0, 1, 2, 3. An index of 4 throws ArrayIndexOutOfBoundsException. The condition i <= array.length instead of i < array.length is the standard mistake.

// modules has 4 elements: indices 0, 1, 2, 3 for (int i = 0; i <= modules.length; i++) { // ❌ <= tries index 4 — crashes modules[i].update(); } for (int i = 0; i < modules.length; i++) { // ✅ < stops at index 3 modules[i].update(); } // Even better when you don't need the index — let Java handle bounds for (SwerveModule m : modules) { m.update(); } // ✅ for-each never goes out of bounds

Bug 2: Infinite loop from a condition that never changes

A while loop whose condition depends on a variable that never gets updated inside the loop will run forever. In robot code, this freezes the periodic method.

// ❌ Infinite loop — i is never incremented int i = 0; while (i < 4) { modules[i].update(); // forgot: i++; } // ✅ Increment must be present and reachable int i = 0; while (i < 4) { modules[i].update(); i++; // loop terminates after 4 iterations }

Bug 3: Loop variable shadowing an outer variable

Declaring a loop variable with the same name as a class field silently hides the field inside the loop. The loop operates on its local copy; the field is unchanged. This produces behavior that looks correct in isolation but fails when you check the field's value later.

private int moduleCount = 4; // class field public void configModules() { for (int moduleCount = 0; moduleCount < modules.length; moduleCount++) { // ❌ shadows the field modules[moduleCount].configure(); } // The class field moduleCount is still 4 — the loop variable is gone // Any code that reads this.moduleCount outside thinks nothing happened } // ✅ Use a different loop variable name to avoid ambiguity for (int i = 0; i < modules.length; i++) { modules[i].configure(); }

🔌 System Check

⚙️ Loops and the 20ms Budget
  • Zero blocking loops in periodic methods. Any loop that waits for a hardware event — a sensor reading, a motor reaching a setpoint, a timer — belongs outside periodic code. Convert it to a state check: test the condition each periodic call and return. This is non-negotiable.
  • Loops in periodic code must complete in well under 1ms. Iterating over four swerve modules, six motors, or eight CAN devices takes microseconds. Iterating over a large dataset, a long string, or anything dynamically sized requires profiling before it goes into periodic code.
  • Always use for-each when you don't need the index. for (Motor m : motors) is immune to off-by-one errors and bounds violations. Prefer it over indexed for loops for collection iteration whenever the index is not needed.
  • robotInit() loops are time-budgeted differently. A loop in robotInit() that takes 200ms is fine — it runs once at startup. The same loop in teleopPeriodic() is a match-ending bug. Know which method you're in.
  • burnFlash() inside init loops, never inside periodic. Writing configuration to motor controller flash memory (REV) or persistent storage (CTRE) is a slow operation — up to 200ms per motor. It belongs in robotInit(), inside a loop that touches all motors exactly once.

Knowledge Check

Click an answer to check your understanding.

A student puts this code inside teleopPeriodic(): while (!beamBreak.get()) { intakeMotor.set(0.6); }. The beam break is slow to trigger. What happens to the robot?
  • 1The robot continues operating normally — WPILib runs periodic methods on a separate thread so a slow loop doesn't affect timing
  • 2The periodic method doesn't return until the beam break triggers — WPILib's watchdog fires, the Driver Station loses the heartbeat, and the robot disables itself
  • 3The loop exits automatically after 20ms because WPILib enforces the timing window
  • 4The code compiles but throws a runtime exception on the roboRIO because blocking loops are not permitted in WPILib
An array of swerve modules has 4 elements. What is the output of this loop, and does it throw an exception?

for (int i = 0; i <= modules.length; i++) { System.out.println(i); }
  • 1Prints 0, 1, 2, 3 — no exception
  • 2Prints 1, 2, 3, 4 — no exception
  • 3Prints 0, 1, 2, 3, 4 — then throws ArrayIndexOutOfBoundsException when the loop body tries to access modules[4], which doesn't exist
  • 4Prints 0, 1, 2, 3, 4 — no exception, because println just prints i, not modules[i]
You need to apply current limits to all six motors at startup, then check the highest current draw during every teleop cycle. Which approach correctly places loops for each task?
  • 1Both loops go in teleopPeriodic() — they run fast enough to fit in 20ms
  • 2Both loops go in robotInit() — initialization should always happen before teleop
  • 3The configuration loop goes in robotInit() (runs once at startup); the current-check loop goes in a helper method called from teleopPeriodic() (runs fast, completes in microseconds, returns a value each cycle)
  • 4Configuration loops must use while; current-check loops must use for — the loop type determines which periodic method they belong in
💪 Practice Prompt

Loop Audit and Redesign

The following Robot.java skeleton contains three loop-related problems of increasing severity. Identify each one, explain the physical consequence on the robot, and write the corrected version.

public class Robot extends TimedRobot { private TalonFX[] driveMotors = new TalonFX[4]; private CANcoder[] steerEncoders = new CANcoder[4]; private boolean gamePieceHeld = false; private static final int DRIVE_CURRENT_LIMIT = 50; private static final double INTAKE_SPEED = 0.7; @Override public void robotInit() { // Problem 1 — find and fix the bounds error for (int i = 0; i <= driveMotors.length; i++) { driveMotors[i] = new TalonFX(i); driveMotors[i].setCurrentLimit(DRIVE_CURRENT_LIMIT); } } @Override public void teleopPeriodic() { // Problem 2 — find and fix the blocking loop if (controller.getAButton()) { intakeMotor.set(INTAKE_SPEED); while (!beamBreak.get()) { } intakeMotor.set(0.0); gamePieceHeld = true; } // Problem 3 — find and fix the hidden variable issue double maxCurrent = 0.0; for (int driveMotors = 0; driveMotors < 4; driveMotors++) { double amps = this.driveMotors[driveMotors].getStatorCurrent().getValueAsDouble(); if (amps > maxCurrent) { maxCurrent = amps; } } SmartDashboard.putNumber("Max Drive Amps", maxCurrent); } }
  1. For each of the three problems: name it, explain in one sentence what it does to the robot at runtime, and write the corrected code.
  2. Problem 2 requires a redesign, not just a syntax fix. Implement the state-based alternative for the intake sequence. Your solution must: keep teleopPeriodic() returning within 20ms every cycle, correctly set gamePieceHeld to true when the beam break triggers, and stop the intake motor immediately at that point.
  3. Extract the max-current logic from Problem 3 into its own private double getMaxDriveCurrent() helper method. Show how you call it from teleopPeriodic().
  4. Bonus: Add a for-each loop in robotInit() that prints the CAN ID of every drive motor to the console using System.out.println(). Then add a second for-each that uses continue to skip any motor whose CAN ID is greater than 3, logging a warning for skipped motors.