Command-Based Pitfalls and How to Avoid Them
Command-based is powerful but has sharp edges that bite nearly every new programmer. Here are the most common ones and the fix.
Mistake 1: Default command without a requirement
Section titled “Mistake 1: Default command without a requirement”A default command must require its subsystem , or the scheduler throws an IllegalArgumentException complaining that default commands must require their subsystem. The fix is almost always to build the command from the subsystem’s own factory (run, runOnce) so the requirement is added automatically:
// Wrong: a bare command that doesn't require the drivetrainm_drive.setDefaultCommand(new RunCommand(() -> m_drive.stop()));// Right: factory on the subsystem adds the requirement for youm_drive.setDefaultCommand(m_drive.run(() -> m_drive.stop()));Mistake 2: Forgetting requirements anywhere
Section titled “Mistake 2: Forgetting requirements anywhere”If two commands touch the same subsystem but only one declares the requirement, the scheduler won’t know they conflict and both run – fighting over the motor. Every command that reads or writes a subsystem must list it in addRequirements() (or be built from that subsystem’s factory). Correct requirements are what let the scheduler deschedule the default command when a real command is triggered.
Mistake 3: Reusing a single command instance
Section titled “Mistake 3: Reusing a single command instance”A Command object can only be scheduled in one place at a time. Storing one instance and binding it to two triggers causes confusing ‘command already scheduled’ behavior. Prefer command factories -- methods that return a new command each call:
public Command scoreL4() { return runOnce(() -> setGoal(L4)); }Mistake 4: Putting logic in default commands
Section titled “Mistake 4: Putting logic in default commands”Keep default commands trivial – stop or hold position. Decision logic belongs in triggers composed with and(), or(), negate(), and smoothed with debounce(). Frame triggers as problem-domain yes/no questions (hasGamePiece, isAtSpeed), and never expose raw sensor values to RobotContainer.
Mistake 5: Comparing floats with ==
Section titled “Mistake 5: Comparing floats with ==”encoder.getPosition() == 10.0 is almost never true. Use a tolerance:
if (MathUtil.isNear(10.0, encoder.getPosition(), 0.05)) { ... }Mistake 6: Expensive work repeated per trigger
Section titled “Mistake 6: Expensive work repeated per trigger”Every trigger condition is polled each loop. If a trigger calls an expensive method (a CAN query, a vision computation), and several triggers ask the same question, you pay that cost repeatedly. Compute it once per loop in periodic(), cache it, and have triggers read the cached value.
Getting these six right eliminates the majority of ‘the robot does weird things’ command bugs before they ever reach the field.
Key takeaways
Section titled “Key takeaways”- Default commands must require their subsystem – build them from the subsystem’s run()/runOnce() factories.
- Every command must declare requirements so the scheduler can resolve conflicts and deschedule defaults.
- Use command factories that return new instances; never reuse one command object across bindings.
- Keep logic in debounced triggers (not default commands), use MathUtil.isNear() for floats, and cache expensive trigger values.
This lesson was adapted from learnfrc.com.
