MOVEMENT · ACTION RANGE DEBUGGING

How to Fix ERR_NOT_IN_RANGE in Screeps

Identify which action returned ERR_NOT_IN_RANGE, use its real range, save the moveTo() result separately, and retry the original action on a later tick instead of assuming movement is immediate.

Verification statusChinese source article: Reviewed in full · Official docs: Checked — Creep action ranges, moveTo(), game-loop timing · Range boundary: Range 1 and range 3 actions are handled separately

VERIFICATION

Evidence and test status

Chinese source article
Reviewed in full
Official docs
Checked — Creep action ranges, moveTo(), game-loop timing
Range boundary
Range 1 and range 3 actions are handled separately
Source correction
Current moveTo() return table does not list ERR_NO_BODYPART
JavaScript syntax
Passed
Offline branch review
Passed — missing objects, range 1, range 3, movement and non-range errors
Screeps Console test
Pending
Live movement and action retry
Pending
Last verified
July 25, 2026

Quick answer

ERR_NOT_IN_RANGE is -9. For a Creep action, it means the target exists but the Creep is outside that method's required distance. Save the action result, call moveTo() only when the result is exactly ERR_NOT_IN_RANGE, use the action's real range, save the movement result separately, return from the branch, and retry the original action on a later tick.

Identify which method returned -9

Do not log only a bare number:

const harvestResult = creep.harvest(source);

console.log(JSON.stringify({
  method: 'harvest',
  creepName: creep.name,
  targetId: source.id,
  harvestResult
}));

The same constant can be returned by many Creep actions, but the valid distance and other prerequisites differ. A structure method that returns a range-related error cannot solve it by calling moveTo() on the structure.

Common Creep action ranges

ActionUsable distanceSuggested movement range
harvest()Adjacent{ range: 1 }
withdraw()Adjacent{ range: 1 }
transfer()Adjacent{ range: 1 }
pickup()Same tile or adjacent{ range: 1 }
attack() and heal()Range 1{ range: 1 }
build(), repair(), and upgradeController()Range 3{ range: 3 }
rangedAttack() and rangedHeal()Range 3{ range: 3 }

The range option tells pathfinding where it may stop. It does not change the original action's rules. Using range 1 for an upgrader can work, but it sends the Creep closer than necessary and may crowd the Controller area.

Move now and retry on the next tick

Current tick:
call action
→ ERR_NOT_IN_RANGE
→ submit moveTo()
→ return

Later tick:
read the new position
→ call the action again

Screeps scripts read the game state for the current tick. Calling moveTo() does not change creep.pos during the same JavaScript execution:

creep.moveTo(source);
creep.harvest(source);

The second line still checks the old position. A clear loop attempts the action first, submits movement only for ERR_NOT_IN_RANGE, and lets the next tick retry.

Complete range-1 example

State impact: this script may submit one harvest action or one movement order for Harvester1. It does not write Memory.

function moveForAction(
  creep,
  target,
  desiredRange,
  label
) {
  if (creep.getActiveBodyparts(MOVE) <= 0) {
    return {
      status: 'no-active-move-part'
    };
  }

  const moveResult = creep.moveTo(target, {
    range: desiredRange,
    reusePath: 10
  });

  if (
    moveResult !== OK
    && moveResult !== ERR_TIRED
  ) {
    console.log(JSON.stringify({
      type: 'movement-failed',
      creepName: creep.name,
      label,
      moveResult
    }));
  }

  return {
    status: moveResult === OK
      ? 'move-submitted'
      : 'move-not-submitted',
    moveResult
  };
}

function runHarvester(creep) {
  if (!creep || creep.spawning) {
    return {
      status: 'creep-unavailable'
    };
  }

  const source = creep.pos.findClosestByPath(
    FIND_SOURCES_ACTIVE
  );

  if (!source) {
    return {
      status: 'active-source-not-found'
    };
  }

  const harvestResult = creep.harvest(source);

  if (harvestResult === ERR_NOT_IN_RANGE) {
    return {
      harvestResult,
      ...moveForAction(
        creep,
        source,
        1,
        'Source'
      )
    };
  }

  if (harvestResult !== OK) {
    console.log(JSON.stringify({
      type: 'harvest-failed',
      creepName: creep.name,
      sourceId: source.id,
      harvestResult
    }));
  }

  return {
    status: harvestResult === OK
      ? 'harvest-submitted'
      : 'harvest-failed',
    harvestResult
  };
}

module.exports.loop = function () {
  runHarvester(Game.creeps.Harvester1);
};

The action and movement results remain separate. A pathfinding problem is not misreported as a harvesting problem.

Complete range-3 example

function runUpgrader(creep) {
  if (!creep || creep.spawning) {
    return {
      status: 'creep-unavailable'
    };
  }

  const controller = creep.room.controller;

  if (!controller || controller.my !== true) {
    return {
      status: 'owned-controller-not-found'
    };
  }

  const upgradeResult =
    creep.upgradeController(controller);

  if (upgradeResult === ERR_NOT_IN_RANGE) {
    const moveResult = creep.moveTo(controller, {
      range: 3,
      reusePath: 10
    });

    return {
      status: moveResult === OK
        ? 'move-submitted'
        : 'move-not-submitted',
      upgradeResult,
      moveResult
    };
  }

  return {
    status: upgradeResult === OK
      ? 'upgrade-submitted'
      : 'upgrade-failed',
    upgradeResult
  };
}

The same movement range applies to normal building and repair actions. Keep their resource, ownership, and body checks separate.

Read the movement result separately

moveTo() resultMeaningNext step
OKThe movement order was scheduled.Read position on a later tick.
ERR_TIREDThe Creep has fatigue.Wait and inspect load, terrain, and MOVE ratio.
ERR_NO_PATHNo route to the requested range was found.Inspect range, obstacles, callbacks, and route limits.
ERR_NOT_FOUNDnoPathFinding was used without a reusable cached path.Allow a fresh path search.
ERR_INVALID_TARGETThe movement target is invalid.Validate the target and its position.
ERR_BUSYThe Creep is still spawning.Wait until spawning finishes.
ERR_NOT_OWNERThe Creep is not yours.Validate the selected object.

OK does not prove that the Creep arrived or even changed position after tick processing. Continue with the moveTo() OK diagnostic guide when several later ticks show no progress.

If the Creep is already in range

const currentRange = creep.pos.getRangeTo(target);
const adjacent = creep.pos.isNearTo(target);
const withinRangeThree = creep.pos.inRangeTo(
  target,
  3
);

When the required range is already satisfied but the action still fails, more movement is not the solution. Inspect active body parts, carried resources, target capacity, target type, ownership, spawning state, or the method's other documented return codes.

Current moveTo() return-code correction

The Chinese source listed ERR_NO_BODYPART as a possible moveTo() result. The current official Creep.moveTo() return table does not list that code. This English article still checks creep.getActiveBodyparts(MOVE) because a Creep without an active MOVE part cannot make normal movement progress, but it does not attribute an undocumented return value to moveTo().

Debugging checklist

  • Record the exact action method and its return value.
  • Validate the Creep and target before calling either method.
  • Use range 1 or range 3 according to the real action.
  • Enter the movement branch only for ERR_NOT_IN_RANGE.
  • Save actionResult and moveResult separately.
  • Return after scheduling movement.
  • Retry the original action on a later tick.
  • Precheck active MOVE parts without inventing a moveTo() return code.
  • Do not keep moving when another prerequisite is failing.
  • Use the English error-code reference for shared constants.

Scope and next steps

This guide does not design CostMatrix rules, cross-room routing, traffic coordination, path caches, or a global movement scheduler. Continue with diagnosing a movement order that returns OK but shows no progress.

Frequently asked questions

Does -9 mean the target is invalid?

No. It means the Creep is outside the action's required distance.

Should every action stop at range 1?

No. Building, repair, Controller upgrading, ranged attacks, and ranged healing work at range 3.

Can movement and the action finish in one script execution?

The later action still reads the current tick's old position. Retry on a later tick.

Does moveTo() return ERR_NO_BODYPART?

The current official return table does not list it. Check active MOVE parts separately.

Official documentation

SOURCE AND SCOPE

Review the source, evidence, or next system

This English guide is rewritten for a focused search intent while preserving the technical scope and verification boundaries of its Chinese source. Live-room evidence is claimed only when the verification record says it exists.