MOVEMENT · PATHFINDING DEBUGGING

How to Debug ERR_NO_PATH Without Confusing Search Cost with Movement Cost

CURATED PATH

These links are maintained for a concrete prerequisite, failure mode, debugging step, or next lesson instead of generic topic similarity.

Movement

Recommended next steps

Interpret PathFinder.search() and incompleteSearch semanticsInspect CostMatrix walkability and costsCore debuggingConfirm the goal rangeGoal boundaryCheck room-level routing for cross-room travelCross-room path
Verification statusOfficial documentation: Checked August 12, 2026 — Creep.moveTo(), PathFinder.search(), CostMatrix, Game.map.findRoute(), range, maxOps, maxRooms, roomCallback, and incomplete · Static code review: Passed — search failure surfaces, PathFinder default weights, movement-fatigue boundary, walkable Ramparts, callbacks, and cross-room route checks reviewed · Chinese source article: Existing bilingual mapping retained; this English-only pass did not use the Chinese page as live-game evidence

VERIFICATION

Evidence and test status

Official documentation
Checked August 12, 2026 — Creep.moveTo(), PathFinder.search(), CostMatrix, Game.map.findRoute(), range, maxOps, maxRooms, roomCallback, and incomplete
Static code review
Passed — search failure surfaces, PathFinder default weights, movement-fatigue boundary, walkable Ramparts, callbacks, and cross-room route checks reviewed
Chinese source article
Existing bilingual mapping retained; this English-only pass did not use the Chinese page as live-game evidence
Console test pending
Not run in this editorial pass
Screeps Console test
Pending — no real-shard Console transcript was collected for this revision
Live multi-tick verification pending
No real-shard terrain, callback, or cross-room route trace was collected for this revision
Last verified
August 12, 2026

Use this guide when

Use this guide when creep.moveTo() returns ERR_NO_PATH, or when a direct PathFinder.search() returns incomplete: true. Do not use it for a movement call that returned OK and then failed to make position progress; that belongs in the accepted-movement diagnostic.

The first question is not “How do I increase maxOps?” It is “Which path boundary actually failed?” A wrong target range, a room rejected by a callback, a CostMatrix blocker, a deliberately disabled path search, and a genuinely exhausted search need different fixes.

Separate three failure surfaces

SignalWhat it meansFirst place to look
moveTo() === ERR_NO_PATHThe movement helper could not find a path for the current request.Target, range, path options, obstacles, callbacks.
moveTo({ noPathFinding: true }) === ERR_NOT_FOUNDNo reusable cached path was available while new pathfinding was disabled.Cache/reuse policy, not CostMatrix tuning first.
PathFinder.search(...).incomplete === trueThe search did not complete the requested goal; the returned path can still be partial.Goal range, limits, callbacks, matrix, room route.
const moveResult = creep.moveTo(target, {
  range: desiredRange,
  reusePath: 0
});

const search = PathFinder.search(
  creep.pos,
  { pos: target.pos, range: desiredRange }
);

console.log(JSON.stringify({
  moveResult,
  pathLength: search.path.length,
  ops: search.ops,
  cost: search.cost,
  incomplete: search.incomplete
}));

A non-empty search.path is not proof of success. Check incomplete before treating the path as a completed route.

Validate the goal range first

Many Screeps targets occupy tiles a Creep cannot stand on. The official PathFinder documentation specifically warns that an unwalkable goal such as a Source should use a range of at least 1. Requesting range: 0 can make an otherwise reachable task impossible.

function validGoal(target, range) {
  return Boolean(
    target?.pos
    && Number.isInteger(range)
    && range >= 0
  );
}

const goal = {
  pos: source.pos,
  range: 1
};

Use the action's actual range: adjacent actions commonly need range 1, while build, repair, and Controller upgrading use range 3. If the Creep already satisfies the requested range, an empty path can be correct because no movement is needed.

Do not confuse search cost with movement fatigue

PathFinder cost is a route-selection weight. Movement fatigue is a Creep movement mechanic. They can use similar numbers, but they answer different questions.

SystemPlainSwampRoadPurpose
Standalone PathFinder.search() defaults15Only preferred if your matrix gives the road a lower costChoose a route
Actual Creep movement fatigue rate2101Determine fatigue generated by a move

If you deliberately run PathFinder with plainCost: 2, swampCost: 10, and road tiles set to 1, you are choosing search weights that make roads cheaper than plains. Those values are not the standalone PathFinder defaults, and the resulting search.cost is not a live measurement of Creep fatigue.

const search = PathFinder.search(
  creep.pos,
  { pos: target.pos, range: desiredRange },
  {
    // Chosen search weights so road cost 1 is preferred.
    // PathFinder defaults are plain 1 / swamp 5.
    plainCost: 2,
    swampCost: 10,
    roomCallback(roomName) {
      const room = Game.rooms[roomName];
      if (!room) return undefined;
      return buildDiagnosticMatrix(room);
    }
  }
);

For body weight and fatigue calculations, use the movement-speed guide instead of reading PathFinder cost as a speed metric.

Read the complete PathFinder result

Record the result before changing search limits:

const result = PathFinder.search(
  creep.pos,
  { pos: target.pos, range: desiredRange },
  {
    maxOps: 2000,
    maxRooms: 16
  }
);

console.log(JSON.stringify({
  pathLength: result.path.length,
  ops: result.ops,
  cost: result.cost,
  incomplete: result.incomplete
}));

2000 operations and 16 rooms are the documented PathFinder defaults. They are not recommended production values for every bot. If a constrained diagnostic uses lower values, an incomplete path may be caused by your constraint rather than by an impossible world route.

Build a diagnostic CostMatrix

function buildDiagnosticMatrix(room) {
  const costs = new PathFinder.CostMatrix();

  for (const structure of room.find(FIND_STRUCTURES)) {
    if (structure.structureType === STRUCTURE_ROAD) {
      costs.set(structure.pos.x, structure.pos.y, 1);
      continue;
    }

    const walkableRampart =
      structure.structureType === STRUCTURE_RAMPART
      && (
        structure.my === true
        || structure.isPublic === true
      );

    const walkable =
      structure.structureType === STRUCTURE_CONTAINER
      || walkableRampart;

    if (!walkable) {
      costs.set(structure.pos.x, structure.pos.y, 255);
    }
  }

  return costs;
}

A CostMatrix value of 255 is unwalkable. Do not mark your own private Ramparts as blocked: your Creeps can pass them. Public Ramparts are also walkable. For a diagnostic matrix, keep this walkability rule visible so a Boolean mistake cannot silently remove the only route.

Check callbacks and search limits

A roomCallback is a routing decision, not just a performance hook. Returning false rejects that room from the PathFinder search. Returning undefined leaves that room to default terrain handling when you have no custom matrix.

  • Log which room names your callback rejects.
  • Check whether maxRooms excludes the destination or an unavoidable corridor.
  • Record ops before increasing maxOps.
  • Do not replace a broken matrix with an unlimited search budget.

Check the room-level route

const route = Game.map.findRoute(
  creep.pos.roomName,
  target.pos.roomName
);

if (route === ERR_NO_PATH) {
  console.log('No room-level route was found.');
}

If your route callback returns Infinity for a required room, a room-level route can fail before local tile costs matter. Keep strategic room routing separate from local CostMatrix debugging.

Handle noPathFinding separately

const cachedResult = creep.moveTo(target, {
  range: desiredRange,
  noPathFinding: true
});

if (cachedResult === ERR_NOT_FOUND) {
  const freshResult = creep.moveTo(target, {
    range: desiredRange,
    noPathFinding: false
  });

  console.log(JSON.stringify({
    cachedResult,
    freshResult
  }));
}

ERR_NOT_FOUND in this branch means the cache-only request had no usable path. It is not interchangeable with ERR_NO_PATH.

Separate traffic from search failure

A valid path can exist and moveTo() can return OK while the Creep later remains on the same tile because movement resolution, another movement call, fatigue, or traffic changed the outcome. That is not evidence that PathFinder failed. Track room-aware positions across later ticks and inspect the final movement intent instead.

Minimal diagnostic workflow

  1. Confirm the target exists and the requested range is reachable.
  2. Separate ERR_NO_PATH, cache-only ERR_NOT_FOUND, and PathFinder.incomplete.
  3. Inspect path.length, ops, cost, and incomplete.
  4. Review CostMatrix blockers and owned/public Ramparts.
  5. Log roomCallback rejections and search limits.
  6. For cross-room work, check Game.map.findRoute().
  7. Only after the search succeeds, move on to traffic/fatigue diagnostics.

What this guide does not prove

This revision has current official-documentation review and offline code review. It does not include a real-shard path trace showing a particular room, CostMatrix, or cross-room route failing and then succeeding. Console and live multi-tick verification remain pending.

Official documentation

SOURCE AND SCOPE

Review the source, evidence, or next system

This English guide keeps the technical scope and verification boundaries of its Chinese source. The verification record shows which checks were completed and which runtime observations are still pending.