OPERATIONS · ROOM ERROR ISOLATION

How to Isolate One Room Error Without Stopping Every Other Room

Verification statusChinese source article: Reviewed in full · Official docs: Checked — repeated main-loop execution, later command resolution, CPU execution boundaries, and Game.notify() limits · Language semantics: Checked — try...catch handles thrown JavaScript values, not ordinary Screeps API return codes

VERIFICATION

Evidence and test status

Chinese source article
Reviewed in full
Official docs
Checked — repeated main-loop execution, later command resolution, CPU execution boundaries, and Game.notify() limits
Language semantics
Checked — try...catch handles thrown JavaScript values, not ordinary Screeps API return codes
Evidence boundary
Isolation limits blast radius; it does not prove that the failed task completed or that an external notification arrived
JavaScript syntax
Passed by the repository code-block check
Offline guard review
Passed by repository assertions for continuation, cooldown, retry, non-Error throws, and rate-limited logging
Screeps Console test
Pending
Live multi-room, CPU cost, global reset, and notification delivery test
Pending
Last verified
August 6, 2026

Quick answer

Wrap each owned room, independent subsystem, or similarly recoverable execution unit in its own try...catch boundary. When one unit throws, save a bounded structured record and continue the loop. Keep ordinary Screeps OK and ERR_* results in explicit branches, and keep CPU protection in a separate scheduler.

For optional work, add a small failure window, log interval, cooldown, and automatic retry. For critical Spawn, harvesting, Controller safety, or defense work, prefer next-tick retry instead of a long automatic pause.

Why later rooms stop

A loop does not automatically skip a failed iteration when a called function throws. Without a matching catch, control leaves the current call chain before later rooms run.

module.exports.loop = function () {
  for (const room of Object.values(Game.rooms)) {
    if (room.controller?.my !== true) {
      continue;
    }

    runRoom(room);
  }
};

If runRoom(W1N1) reads anchor.x while anchor is undefined, the later W2N2 and W3N3 calls are not reached in that execution. The next tick starts the main loop again, but unchanged input can reproduce the same failure.

Separate three failure types

FailureExampleCorrect handling
JavaScript exceptionTypeError, ReferenceError, explicit throwCatch at a recoverable boundary, record evidence, fix the cause
Screeps API return codeERR_NOT_IN_RANGE, ERR_FULLSave the returned number and branch explicitly
CPU execution boundaryLater work is not reached after the tick budget is exhaustedMeasure, prioritize, throttle, and degrade separately
const result = creep.transfer(
  target,
  RESOURCE_ENERGY
);

if (result === ERR_NOT_IN_RANGE) {
  return {
    status: 'moving',
    result,
    moveResult: creep.moveTo(target)
  };
}

return {
  status: result === OK
    ? 'submitted'
    : 'failed',
  result
};

An API return code does not become an exception merely because the call is inside try...catch.

Why one outer catch is insufficient

module.exports.loop = function () {
  try {
    runRoom(Game.rooms.W1N1);
    runRoom(Game.rooms.W2N2);
    runRoom(Game.rooms.W3N3);
  } catch (error) {
    console.log(error);
  }
};

If W1N1 throws, control jumps to the single catch and then leaves the function. The catch prevents an uncaught exception from escaping, but it does not resume at W2N2.

Add the minimum room boundary

function runOwnedRooms(runRoom) {
  const rooms = Object.values(Game.rooms)
    .filter(room => room.controller?.my === true)
    .sort((left, right) =>
      left.name.localeCompare(right.name)
    );

  const outcomes = [];

  for (const room of rooms) {
    try {
      outcomes.push({
        roomName: room.name,
        ok: true,
        value: runRoom(room)
      });
    } catch (error) {
      outcomes.push({
        roomName: room.name,
        ok: false,
        errorName: error instanceof Error
          ? error.name
          : 'NonErrorThrow',
        message: error instanceof Error
          ? error.message
          : String(error)
      });
    }
  }

  return outcomes;
}

This is the smallest useful blast-radius boundary: one failure becomes one result, while the loop continues. It still needs rate limits and bounded state before production use.

Store bounded failure state

A useful optional-task record needs only enough data to decide whether to log, pause, retry, and report recovery:

function getGuardState(key) {
  Memory.runtimeGuard ??= { units: {} };
  Memory.runtimeGuard.units ??= {};
  Memory.runtimeGuard.units[key] ??= {
    errorTicks: [],
    consecutiveErrors: 0,
    totalErrors: 0,
    disabledUntil: null,
    lastLogAt: null,
    lastSuccessAt: null,
    lastError: null
  };

  return Memory.runtimeGuard.units[key];
}

Do not serialize an entire Room, Creep, or large configuration object. Keep a stable key, tick, short message, bounded stack, counters, and retry time.

Build a reusable runtime guard

function normalizeThrown(thrown) {
  if (thrown instanceof Error) {
    return {
      name: thrown.name || 'Error',
      message: thrown.message || String(thrown),
      stack: typeof thrown.stack === 'string'
        ? thrown.stack.split('\n').slice(0, 6).join('\n')
        : null
    };
  }

  let message;

  try {
    message = typeof thrown === 'string'
      ? thrown
      : JSON.stringify(thrown);
  } catch {
    message = String(thrown);
  }

  return {
    name: 'NonErrorThrow',
    message: message ?? String(thrown),
    stack: null
  };
}

function runGuarded(key, task, options = {}) {
  const config = {
    windowTicks: 100,
    maxErrors: 3,
    cooldownTicks: 50,
    logIntervalTicks: 20,
    breakerEnabled: true,
    ...options
  };
  const state = getGuardState(key);
  const firstTick = Game.time - config.windowTicks + 1;

  state.errorTicks = state.errorTicks.filter(
    tick => tick >= firstTick
  );

  if (
    config.breakerEnabled
    && Number.isInteger(state.disabledUntil)
    && Game.time < state.disabledUntil
  ) {
    return {
      ok: false,
      status: 'cooldown',
      retryAt: state.disabledUntil
    };
  }

  if (
    Number.isInteger(state.disabledUntil)
    && Game.time >= state.disabledUntil
  ) {
    state.disabledUntil = null;
    state.errorTicks = [];
    state.consecutiveErrors = 0;
  }

  try {
    const value = task();
    const recovered = state.consecutiveErrors > 0;
    state.consecutiveErrors = 0;
    state.lastSuccessAt = Game.time;
    state.lastError = null;

    return {
      ok: true,
      status: recovered ? 'recovered' : 'ok',
      value
    };
  } catch (thrown) {
    const error = normalizeThrown(thrown);
    state.errorTicks.push(Game.time);
    state.consecutiveErrors += 1;
    state.totalErrors += 1;
    state.lastError = {
      tick: Game.time,
      ...error
    };

    const breakerTripped =
      config.breakerEnabled
      && state.errorTicks.length >= config.maxErrors;

    if (breakerTripped) {
      state.disabledUntil = Game.time + config.cooldownTicks;
    }

    const logDue =
      !Number.isInteger(state.lastLogAt)
      || Game.time - state.lastLogAt
        >= config.logIntervalTicks;

    if (logDue) {
      console.log(JSON.stringify({
        type: 'runtime-guard-error',
        tick: Game.time,
        key,
        breakerTripped,
        retryAt: state.disabledUntil,
        error
      }));
      state.lastLogAt = Game.time;
    }

    return {
      ok: false,
      status: breakerTripped
        ? 'disabled'
        : 'error',
      retryAt: state.disabledUntil,
      error
    };
  }
}

The numeric thresholds are local policy values, not official recommendations. Tune them from real error frequency and room risk.

Separate critical and optional work

module.exports.loop = function () {
  const rooms = Object.values(Game.rooms)
    .filter(room => room.controller?.my === true);

  for (const room of rooms) {
    const critical = runGuarded(
      'critical:' + room.name,
      () => roomManager.runCritical(room),
      {
        breakerEnabled: false,
        logIntervalTicks: 20
      }
    );

    if (!critical.ok) {
      continue;
    }

    runGuarded(
      'optional:' + room.name,
      () => roomManager.runOptional(room),
      {
        breakerEnabled: true,
        windowTicks: 100,
        maxErrors: 3,
        cooldownTicks: 50,
        logIntervalTicks: 20
      }
    );
  }
};

Critical work should remain small and retryable. Optional visuals, statistics, long-range planning, or market scans can use a cooldown when repeated exceptions would otherwise waste CPU every tick.

Read structured error evidence

FieldQuestion it answers
keyWhich room and task boundary failed?
tickWhen did this exact observation occur?
breakerTrippedDid this failure start a cooldown?
retryAtWhen may the optional task run again?
error.nameWhat JavaScript failure class was observed?
error.messageWhat immediate condition failed?
error.stackWhich bounded call chain reached the failure?

A catch is not a repair. It preserves the rest of the system while evidence guides the real null check, migration, identity fix, visibility guard, or module correction.

Retry after cooldown

Before disabledUntil, the optional task returns cooldown without calling its function. At or after the retry tick, clear the current failure window and attempt the task once. A successful call should record a recovery event; another exception starts a new failure sequence.

function summarizeGuardOutcome(key, outcome) {
  if (outcome.status === 'recovered') {
    return {
      type: 'runtime-guard-recovered',
      tick: Game.time,
      key
    };
  }

  if (outcome.status === 'cooldown') {
    return {
      type: 'runtime-guard-cooldown',
      tick: Game.time,
      key,
      retryAt: outcome.retryAt
    };
  }

  return null;
}

Debugging checklist

  1. Find the first exception, not only the latest repeated line.
  2. Record its room, task key, tick, type, message, and first project stack frame.
  3. Confirm whether one outer catch still skips later rooms.
  4. Check missing Memory fields, destroyed objects, lost visibility, stale IDs, and unrebuilt heap cache.
  5. Handle ERR_* results in ordinary branches.
  6. Measure CPU separately when there is no exception stack.
  7. After the code or input is fixed, observe a recovery and several later healthy ticks.

Scope and limitations

This pattern applies to multiple owned rooms, role dispatchers, optional diagnostics, market scans, remote planning, and other independently recoverable units. It does not recover syntax errors that prevent loading, infinite loops, top-level module initialization failures, CPU termination, shard outages, or a complete Memory schema migration.

Repository syntax and offline control-flow assertions do not prove live Screeps CPU cost, multi-room behavior, global-reset recovery, or external alert delivery. Those remain explicit live-test tasks.

Frequently asked questions

The FAQ below keeps return-code handling, critical-task retry, CPU boundaries, and stack-size decisions separate from exception isolation.

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.