Skip to content

Command-Based Pitfalls and How to Avoid Them

learnfrc.com
learnfrc.comAuthor
Veer Bajaj
Veer BajajMaintainer

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

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.

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.

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