MOVEMENT · PATHFINDING DEBUGGING

How to Debug ERR_NO_PATH in Screeps

Distinguish ERR_NO_PATH, ERR_NOT_FOUND and PathFinder incomplete results; validate target range; correct CostMatrix walkability; inspect callbacks, maxOps, maxRooms and cross-room routes; and separate temporary traffic from a failed search.

Verification statusChinese source article: Reviewed in full · Official docs: Checked — moveTo(), PathFinder, CostMatrix and map routes · Result distinction: ERR_NO_PATH, cached-path ERR_NOT_FOUND and incomplete searches are separated

VERIFICATION

Evidence and test status

Chinese source article
Reviewed in full
Official docs
Checked — moveTo(), PathFinder, CostMatrix and map routes
Result distinction
ERR_NO_PATH, cached-path ERR_NOT_FOUND and incomplete searches are separated
Source correction
Owned or public Ramparts remain walkable in the diagnostic CostMatrix
JavaScript syntax
Passed
Offline path classification
Passed — target, range, reached, callback, cached path, no path and incomplete states
Screeps Console test
Pending
Live terrain, callback and cross-room route test
Pending
Last verified
July 25, 2026

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 false from a PathFinder roomCallback rejects that room.
  • Returning undefined allows the default terrain matrix when no custom room data is available.
  • A low maxOps can stop a difficult search early.
  • A low maxRooms can 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, and incomplete.
  • Separate ERR_NO_PATH from cached-path ERR_NOT_FOUND.
  • Use 255 only for intentionally unwalkable tiles.
  • Allow owned or public Ramparts unless the task has a stricter policy.
  • Return undefined, not false, when default handling is desired.
  • Inspect maxOps and maxRooms.
  • 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.

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.