Quick answer
ERR_NO_PATH is -2. For creep.moveTo(), it means the current path search did not find a route to the requested target range. First validate the target and range, then distinguish it from ERR_NOT_FOUND caused by noPathFinding: true without a cached path and from PathFinder.search().incomplete === true. Inspect CostMatrix blockers, room callbacks, search limits, and the room-level route before increasing CPU limits blindly.
Separate three similar path results
moveTo() → ERR_NO_PATH
The current path search did not find a route.
moveTo({ noPathFinding: true }) → ERR_NOT_FOUND
No reusable cached path was available.
PathFinder.search() → incomplete: true
The search ended without completely reaching the goal range.
These states need different logs and recovery policies. “Try pathfinding again” is not enough when a callback has rejected the only room or the requested range is impossible.
Validate the target and desired range
function validatePathRequest(
target,
desiredRange
) {
if (!target?.pos) {
return {
valid: false,
reason: 'target-invalid'
};
}
if (
!Number.isInteger(desiredRange)
|| desiredRange < 0
) {
return {
valid: false,
reason: 'range-invalid'
};
}
return {
valid: true,
reason: 'ready'
};
}
A Source, Controller, Construction Site, and most Structures occupy a tile that a Creep cannot enter. Asking for range: 0 can make the goal impossible. Use range 1 for adjacent actions and range 3 for build, repair, or Controller upgrading.
An empty path is not always an error
const alreadyInRange = creep.pos.inRangeTo(
target,
desiredRange
);
const path = creep.pos.findPathTo(target, {
range: desiredRange
});
if (path.length === 0 && !alreadyInRange) {
console.log('No path while outside range.');
}
An empty array can mean no movement is needed because the Creep already satisfies the range. Always pair path length with the current range.
Read the complete PathFinder result
const search = PathFinder.search(
creep.pos,
{
pos: target.pos,
range: desiredRange
}
);
console.log(JSON.stringify({
pathLength: search.path.length,
ops: search.ops,
cost: search.cost,
incomplete: search.incomplete
}));
A non-empty search.path does not prove the target range was reached. When incomplete is true, the path can be only the best partial route found before the search stopped.
Classify the path result
function classifyPathResult(input) {
const {
targetExists,
targetHasPosition,
desiredRange,
alreadyInRange,
moveResult,
pathLength,
pathIncomplete,
callbackRejectedRoom
} = input;
if (!targetExists || !targetHasPosition) {
return {
usable: false,
reason: 'target-invalid'
};
}
if (
!Number.isInteger(desiredRange)
|| desiredRange < 0
) {
return {
usable: false,
reason: 'range-invalid'
};
}
if (alreadyInRange) {
return {
usable: true,
reason: 'already-in-range'
};
}
if (callbackRejectedRoom) {
return {
usable: false,
reason: 'callback-rejected-room'
};
}
if (moveResult === ERR_NOT_FOUND) {
return {
usable: false,
reason: 'cached-path-missing'
};
}
if (moveResult === ERR_NO_PATH) {
return {
usable: false,
reason: 'move-no-path'
};
}
if (pathIncomplete === true) {
return {
usable: false,
reason: 'pathfinder-incomplete'
};
}
if (
!Number.isInteger(pathLength)
|| pathLength <= 0
) {
return {
usable: false,
reason: 'path-empty-out-of-range'
};
}
return {
usable: true,
reason: 'path-available'
};
}
Build a correct 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 isWalkableRampart =
structure.structureType === STRUCTURE_RAMPART
&& (
structure.my === true
|| structure.isPublic === true
);
const isWalkableStructure =
structure.structureType === STRUCTURE_CONTAINER
|| isWalkableRampart;
if (!isWalkableStructure) {
costs.set(
structure.pos.x,
structure.pos.y,
255
);
}
}
return costs;
}
Low values are preferred, and 255 is unwalkable. The Chinese source's condition blocked owned private Ramparts because it required both ownership and public status. Your Creeps can move through your own Ramparts, so this corrected diagnostic allows a Rampart when it is owned or public.
Complete same-room diagnostic
State impact: this example performs a PathFinder search and may submit one movement order. It uses reusePath: 0 and a visual path only for diagnosis.
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 || structure.isPublic);
if (
structure.structureType !== STRUCTURE_CONTAINER
&& !walkableRampart
) {
costs.set(
structure.pos.x,
structure.pos.y,
255
);
}
}
return costs;
}
function diagnosePath(
creep,
target,
desiredRange
) {
if (!creep || !target?.pos) {
return {
status: 'object-invalid'
};
}
if (
!Number.isInteger(desiredRange)
|| desiredRange < 0
) {
return {
status: 'range-invalid'
};
}
if (
creep.pos.roomName === target.pos.roomName
&& creep.pos.inRangeTo(target, desiredRange)
) {
return {
status: 'already-in-range'
};
}
const search = PathFinder.search(
creep.pos,
{
pos: target.pos,
range: desiredRange
},
{
maxOps: 4000,
maxRooms: 1,
plainCost: 2,
swampCost: 10,
roomCallback(roomName) {
const room = Game.rooms[roomName];
if (!room) {
return undefined;
}
return buildDiagnosticMatrix(room);
}
}
);
const moveResult = creep.moveTo(target, {
range: desiredRange,
reusePath: 0,
maxOps: 4000,
visualizePathStyle: {
stroke: search.incomplete
? '#ff4444'
: '#00ff88',
opacity: 0.65
}
});
return {
status: search.incomplete
? 'pathfinder-incomplete'
: moveResult === OK
? 'move-submitted'
: 'move-failed',
moveResult,
pathLength: search.path.length,
incomplete: search.incomplete,
ops: search.ops,
cost: search.cost
};
}
module.exports.loop = function () {
const creep = Game.creeps.Worker1;
const target = Game.flags.WorkTarget;
const outcome = diagnosePath(
creep,
target,
1
);
if (outcome.status !== 'move-submitted') {
console.log(JSON.stringify({
type: 'path-diagnostic',
creepName: creep?.name ?? null,
from: creep
? {
roomName: creep.pos.roomName,
x: creep.pos.x,
y: creep.pos.y
}
: null,
to: target?.pos
? {
roomName: target.pos.roomName,
x: target.pos.x,
y: target.pos.y
}
: null,
...outcome
}));
}
};
This is diagnostic code, not a permanent movement architecture. Recalculating and visualizing every path can consume unnecessary CPU.
Check room callbacks and search limits
- Returning
falsefrom a PathFinderroomCallbackrejects that room. - Returning
undefinedallows the default terrain matrix when no custom room data is available. - A low
maxOpscan stop a difficult search early. - A low
maxRoomscan prevent the search from reaching the target room. - Do not permanently set unlimited search budgets; first identify why the route is expensive.
Record ops, cost, and incomplete before changing limits.
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.');
}
A route callback that returns Infinity rejects a room. Accidentally excluding the destination or its only corridor prevents the Creep-level path from succeeding.
Handle noPathFinding correctly
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
}));
}
noPathFinding: true is useful only after a reusable path exists. Using it on the first call commonly produces ERR_NOT_FOUND, not ERR_NO_PATH.
Temporary traffic is usually not ERR_NO_PATH
A route may be found and moveTo() may return OK, yet another Creep occupies the next tile during resolution. That is a traffic or action-order problem. Track unchanged positions and inspect competing movement calls instead of changing CostMatrix rules immediately. Review the no-progress diagnostic.
Debugging checklist
- Validate the target position and desired range.
- Do not request range 0 for an occupied target tile.
- Check whether an empty path means already in range.
- Read PathFinder
path,ops,cost, andincomplete. - Separate
ERR_NO_PATHfrom cached-pathERR_NOT_FOUND. - Use 255 only for intentionally unwalkable tiles.
- Allow owned or public Ramparts unless the task has a stricter policy.
- Return
undefined, notfalse, when default handling is desired. - Inspect
maxOpsandmaxRooms. - Check
Game.map.findRoute()for cross-room tasks. - Separate temporary traffic from a failed search.
Scope and next steps
This guide does not build a production road matrix, traffic reservation system, hostile-room policy, remote intel database, portal route, highway optimizer, or persistent path cache. Live terrain and cross-room testing remain Pending.
Frequently asked questions
Is an empty path always an error?
No. The Creep may already satisfy the requested range.
Does a non-empty PathFinder path prove success?
No. Check incomplete; the path may be partial.
What does roomCallback returning false do?
It rejects the room from the search.
Should owned Ramparts be unwalkable?
No. Your Creeps can pass your own Ramparts, so the diagnostic matrix allows owned or public Ramparts.