Skip to content
Programming, Controls & Sensors·Lesson 43 of 51

Command-Based Pitfalls and How to Avoid Them

The recurring command-based mistakes -- missing requirements, reused command instances, fragile default commands, and float comparisons -- with the correct patterns.

Sign in to track progress, earn XP, and save lessons.

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

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 drivetrain
m_drive.setDefaultCommand(new RunCommand(() -> m_drive.stop()));
// Right: factory on the subsystem adds the requirement for you
m_drive.setDefaultCommand(m_drive.run(() -> m_drive.stop()));

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

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

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 ==

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

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

  • 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.

Lesson quiz

Required

Answer all 3 questions correctly to complete this lesson.

1.In WPILib command-based programming, when is a subsystem's default command scheduled?

2.Why is reusing the same command instance in two different command compositions a pitfall?

3.What requirements does a command composition have relative to its component commands?

Answer every question to submit.