Use this guide when
Use this guide after StructureSpawn.spawnCreep() returns a non-OK code and you need to decide whether the request should wait, change, or fail closed. If you are making your first simple Creep, use the beginner spawn lesson; this page is the return-code troubleshooting layer.
Current return-code boundary
| Return | Documented boundary | Useful first check |
|---|---|---|
OK | The spawn operation was scheduled successfully. | Observe spawn.spawning; do not call the Creep “finished” yet. |
ERR_NOT_OWNER | You do not own this Spawn. | spawn.my. |
ERR_NAME_EXISTS | A Creep already has that name. | Game.creeps[name] and your naming policy. |
ERR_BUSY | The Spawn is already spawning another Creep. | spawn.spawning. |
ERR_NOT_ENOUGH_ENERGY | The energy sources available to this request do not contain enough Energy for the body. | Body cost plus default or explicit energyStructures. |
ERR_INVALID_ARGS | The body is invalid or the required name was not provided. | Body length/constants and name. |
ERR_RCL_NOT_ENOUGH | The current Room Controller level is insufficient to use this Spawn. | spawn.isActive() and current RCL. |
Do not add undocumented meanings to a code just because two failures happen near the same subsystem. In particular, ERR_RCL_NOT_ENOUGH from spawnCreep() is a Spawn-usage/RCL boundary, not a generic “too many Spawns” response.
Interpret ERR_RCL_NOT_ENOUGH precisely
if (spawn.isActive() !== true) {
console.log(JSON.stringify({
type: 'spawn-inactive',
roomName: spawn.room.name,
rcl: spawn.room.controller?.level ?? null
}));
}
The base Structure API says isActive() is false when the Room Controller level is insufficient to use the structure. That makes it a useful diagnostic companion to ERR_RCL_NOT_ENOUGH. Do not rewrite the return code as a construction-limit error.
Diagnose ERR_NOT_ENOUGH_ENERGY with request context
Without energyStructures, the spawn request can draw the required Energy from Spawns and Extensions in the room. If you provide an explicit energyStructures array, those structures define the request's Energy sources and are used in the supplied order.
This matters because room.energyAvailable can look sufficient while an intentionally restricted request still returns ERR_NOT_ENOUGH_ENERGY.
function bodyCost(body) {
if (!Array.isArray(body)) return null;
let total = 0;
for (const part of body) {
const cost = BODYPART_COST[part];
if (!Number.isFinite(cost)) return null;
total += cost;
}
return total;
}
function describeSpawnEnergy(spawn, opts = {}) {
const explicit = Array.isArray(opts.energyStructures);
const selected = explicit ? opts.energyStructures : [];
const selectedEnergyAvailable = explicit
? selected.reduce(
(sum, structure) =>
sum + (
structure?.store?.getUsedCapacity(
RESOURCE_ENERGY
) ?? 0
),
0
)
: null;
return {
mode: explicit ? 'explicit' : 'room-default',
roomEnergyAvailable: spawn.room.energyAvailable,
selectedEnergyAvailable,
selectedStructureIds: explicit
? selected.map(structure => structure?.id ?? null)
: null
};
}
console.log(JSON.stringify({
bodyCost: bodyCost(body),
energy: describeSpawnEnergy(spawn, opts)
}));
When energyStructures is explicit, selectedEnergyAvailable is a useful snapshot of those supplied Stores; it is not a replacement implementation of spawnCreep() validation. Invalid structures, ownership/RCL state, busy state, name collisions, and changes between checks still belong to the API result. Without an explicit list, room.energyAvailable remains useful context for the default room-wide Spawn/Extension pool.
Validate body and name without inventing API rules
The current API documents a body of 1–50 valid body-part constants and a name with a maximum length of 100 characters. Keep local validation narrow enough that it does not reject API-valid requests for reasons you invented.
function validateSpawnShape(body, name) {
const validParts = new Set([
WORK, MOVE, CARRY, ATTACK,
RANGED_ATTACK, HEAL, TOUGH, CLAIM
]);
if (
!Array.isArray(body)
|| body.length < 1
|| body.length > 50
|| body.some(part => !validParts.has(part))
) {
return { valid: false, reason: 'body-invalid' };
}
if (typeof name !== 'string' || name.length < 1 || name.length > 100) {
return { valid: false, reason: 'name-invalid' };
}
return { valid: true, reason: 'shape-valid' };
}
Do not try to reproduce every runtime condition in the validator. Ownership, busy state, RCL, name collision, and Energy can change independently and should still be handled from the actual API result.
Do not over-restrict opts.memory
The optional memory field is documented as any. A role object such as { role: 'harvester' } is a common and readable project convention, but it is not an API rule that Memory must be a non-null plain object.
const opts = {
memory: {
role: 'harvester',
homeRoom: spawn.room.name
}
};
If your own bot requires an object schema, label that as a project contract and validate it at that boundary. Do not present it as a spawnCreep() restriction.
The API also documents supplied Creep memory as being stored immediately in Memory.creeps[name]. That makes a Memory entry useful state, but not evidence that the Creep has finished spawning. Use the real spawnCreep() result and spawn.spawning for the operation boundary; use the beginner spawn lesson for the full multi-tick spawn lifecycle.
Keep dryRun and the real call separate
dryRun: true checks whether the operation is possible at that moment without starting spawning. It does not reserve the name, Spawn, or Energy for a later call.
const dryRunResult = spawn.spawnCreep(
body,
name,
{ ...opts, dryRun: true }
);
let spawnResult = null;
if (dryRunResult === OK) {
spawnResult = spawn.spawnCreep(
body,
name,
opts
);
}
console.log(JSON.stringify({
tick: Game.time,
dryRunResult,
spawnResult
}));
Keep both values. If the dry run succeeds and the real call does not, the correct conclusion is not “Screeps contradicted itself.” It means the two calls are separate checks/submissions and your diagnostic should preserve the exact state and results rather than overwrite one with the other.
A useful diagnostic wrapper
function requestCreep(spawn, body, name, opts = {}) {
if (!spawn) {
return { status: 'spawn-missing' };
}
const shape = validateSpawnShape(body, name);
if (!shape.valid) {
return { status: shape.reason };
}
const dryRunResult = spawn.spawnCreep(
body,
name,
{ ...opts, dryRun: true }
);
if (dryRunResult !== OK) {
return {
status: 'dry-run-rejected',
dryRunResult,
spawnActive: spawn.isActive(),
busy: Boolean(spawn.spawning),
bodyCost: bodyCost(body),
energy: describeSpawnEnergy(spawn, opts)
};
}
const spawnResult = spawn.spawnCreep(
body,
name,
opts
);
return {
status: spawnResult === OK
? 'spawn-request-accepted'
: 'spawn-request-rejected',
dryRunResult,
spawnResult,
spawnActive: spawn.isActive(),
busy: Boolean(spawn.spawning),
bodyCost: bodyCost(body),
energy: describeSpawnEnergy(spawn, opts)
};
}
This wrapper is diagnostic, not a full spawn queue. A production scheduler still needs demand, body policy, replacement timing, unique names, and retry rules.
Do not retry every error the same way
| Code | Typical policy class |
|---|---|
ERR_BUSY | Wait for the Spawn to become free. |
ERR_NOT_ENOUGH_ENERGY | Wait, reduce the body, or change the explicit Energy-source policy. |
ERR_NAME_EXISTS | First decide whether that name already represents the unit your scheduler wanted. Reuse the existing assignment when appropriate; generate another name only when you truly need another Creep. |
ERR_INVALID_ARGS | Fix the request; blind retry will repeat the same invalid input. |
ERR_NOT_OWNER | Fix Spawn selection. |
ERR_RCL_NOT_ENOUGH | Fix/await the RCL and structure-usability condition; do not treat it as an Energy retry. |
What static review does not prove
This revision verifies the current documented return boundary and code branches offline. It does not contain a live Spawn transcript showing dry-run acceptance, a real submission, and later completion. Console and live multi-tick verification remain pending.