Skip to content

Debugging

Something isn’t working. This page covers the tools for finding out why: printing, where output shows up, how to read an error, and the mistakes almost everyone makes at first.

print(...) and warn(...) from @rally/sdk write a line to the game’s output, joining their arguments with spaces. warn marks the line as a warning. Values print in a readable way:

Value Prints as
a string, number or boolean itself
an instance its path: Workspace.Map.Door
a Vector3 or Vector2 Vector3(1, 2, 3)
a Color3 Color3(#ff5a5f)
an array or plain object JSON: [1,2,3], {"ann":3}
an Error its message and stack trace
undefined nothing — wrap it as String(value) to see it
ServerScriptService/PrintDemo.server.ts
import { Color3, getChild, Part, print, Vector3, warn, workspace } from '@rally/sdk';
const door = workspace.findFirstChild('Door', true);
print('door:', door); // door: Workspace.Map.Door
print('at', new Vector3(1, 2, 3), 'in', Color3.fromHex('#ff5a5f')); // at Vector3(1, 2, 3) in Color3(#ff5a5f)
print('scores', { ann: 3, bo: 5 }, [1, 2, 3]); // scores {"ann":3,"bo":5} [1,2,3]
warn('low on coins:', 2); // shown as a warning
// There's no error() function: throw an Error. The rest of this script stops; the game goes on.
const gate = getChild(workspace, 'Gate', Part); // throws: Workspace has no child 'Gate' (expected a Part)
print('never printed', gate);

There’s no error() function: throw new Error('…') instead. An error nothing catches is logged in red, with a stack trace, and the game keeps running — only the code that threw stops.

Printing has a budget: a game server keeps at most 200 lines a second and drops the rest, with a warning saying how many it dropped. Don’t print every step; print when something happens.

The Output panel under the viewport shows what your scripts print while you test:

  • With Play, the server’s scripts and your device’s scripts run together in your browser, so Output shows everything.
  • With Test on a real game server, Output shows the server’s lines — they start with [server] — and the lines from the first test player’s device.
  • Compile errors show up here too, when you press Play, start a test or click Check.

Each line has the time it was printed; warnings and errors stand out in color, and the panel’s header counts the errors. Clear empties it. Lines that point at a place in a script are links: click one to open the script at that line.

While playing, press ` (backtick) to open the Developer console, and again to close it. It shows the output of your own device, marked [client]. When you play your own published game, it also shows the server’s output, marked [server] — including the last 200 lines from before you joined, so you can see what happened while the server started. If an error arrives while the console is closed, a badge in the bottom-left corner counts it; click the badge to open the console.

Anyone playing your game can open the console, but they only see their own device’s output. Server output only goes to the game’s owner.

The playtest in Studio has the same console. A few errors — those thrown inside UserInputService handlers, for one — currently show up only there, not in Studio’s Output. If a key press seems to do nothing and Output is quiet, click the playtest and press `.

An error nobody caught is logged as one red entry. Here’s how the error from the PrintDemo script above looks in Studio’s Output:

script PrintDemo: Error: Workspace has no child 'Gate' (expected a Part)
at getChild (rally://ServerScriptService/PrintDemo.js:85:11)
at eval (ServerScriptService/PrintDemo.ts:10:14)
at eval (rally://ServerScriptService/PrintDemo.js:98:3)

The first line says where it came from and what went wrong. The part before the first colon says what was running:

It starts with The error came from
script PrintDemo the top-level code of the script named PrintDemo
an event, like Part.touched or Players.playerAdded a handler connected to that event
RunService.heartbeat a heartbeat handler
task.spawn, task.defer or task a function run by task.spawn, by task.defer, or by a task.delay or task.wait

The lines after it, starting with at, are the stack trace: the chain of calls that led to the error, innermost first. In Studio, lines in your own code point at your TypeScript, as the script’s path, line and column — ServerScriptService/PrintDemo.ts:10:14 is the getChild call on line 10 — and clicking the error jumps there. Lines inside the game API, like getChild itself, keep their compiled rally:// locations: look for the first line that names one of your scripts. In a live game, every line points at the compiled JavaScript, so when a live game logs an error, reproduce it in Studio to find the line.

When you press Play, start a test or click Check, Studio first compiles every script. If one can’t be compiled, nothing runs: Studio says A script has an error (or how many), and Output lists each problem with its place in the script — ServerScriptService/Main.ts:2:7: and the message. Click it to jump there; the editor underlines it in red too. Publish compiles the same way, and refuses with a message naming the first problem until they’re all fixed.

Some you’ll meet:

Message (shortened) What to do
Unexpected ";", Expected ")" but found … A typo: fix the syntax at that spot.
Top-level await is currently not supported … Wrap the code in task.spawn(async () => { … }).
'@game/…' is a Script; only ModuleScripts can be imported Turn the code you want to share into a ModuleScript.
can't find '@game/…' (nothing at …) The import path doesn’t match the tree. Check names and capitals.
two objects here have this name, so it can't be imported unambiguously Rename one of the two scripts with the same name.
a LocalScript can't import '@game/…': it's in ServerStorage, which players never get Move code both sides use to ReplicatedStorage; keep server-only code out of LocalScripts and the modules they import.

Type errors — a misspelled property, a missing instanceof check — don’t stop Play. The editor underlines them and counts them in the bar under the script. Fix them anyway: they’re almost always real bugs.

  • Print with labels and paths. print('door', door, 'open?', door.getAttribute('open')) tells you far more than print(x). Instances print as their path, which settles “which one?”.

  • Give waitForChild a timeout while you build. Without one, a misspelled name waits forever, silently. With one, you get an error that names what’s missing:

    StarterPlayer/StarterPlayerScripts/Typo.client.ts
    import { Folder, ReplicatedStorage, task, waitForChild } from '@rally/sdk';
    task.spawn(async () => {
    // Without a timeout, a typo here would wait forever without a word. With one, you get an error.
    const remotes = await waitForChild(ReplicatedStorage, 'Remtoes', Folder, 5);
    remotes.getChildren();
    });

    After five seconds it logs waitForChild('Remtoes') timed out on ReplicatedStorage.

  • Mark positions in the world. For raycasts, spawn points and anything else spatial, drop a glowing ball where your code thinks something is:

    ServerScriptService/Marker.server.ts
    import { Color3, Part, task, Vector3, workspace } from '@rally/sdk';
    /** Drops a small glowing ball at `position` for a few seconds: a quick way to see a point. */
    function mark(position: Vector3, color = Color3.fromHex('#ff00ff')): void {
    const dot = new Part();
    dot.shape = 'sphere';
    dot.size = new Vector3(0.6, 0.6, 0.6);
    dot.material = 'neon';
    dot.color = color;
    dot.canCollide = false;
    dot.canTouch = false;
    dot.position = position;
    dot.parent = workspace;
    task.delay(3, () => dot.destroy());
    }
    task.spawn(async () => {
    await task.wait(); // physics queries see the world from the first step on
    const hit = workspace.raycast(new Vector3(0, 50, 0), new Vector3(0, -100, 0));
    if (hit) mark(hit.position);
    });
  • Test with two players. Play runs everything in one browser, which hides problems between the server and players’ devices. Test on a real game server with two players catches LocalScripts that assume something exists, changes that only happened on one device, and code that only works for the first player.

Play is the quickest way to test, but because the server and your device run together, a few things behave differently there:

  • RunService.isServer() and RunService.isClient() are both true, and Players.localPlayer is set even in server scripts.
  • Data stores start empty and forget everything when you stop.

A test server behaves like the real thing, except that nothing is saved. See Testing.

What you see What happened
The game server stopped: a script ran too long. A step took too long, or the game kept the server busy for too long — usually a loop that never waits. See Tasks and timing.
The game server stopped: it ran out of memory. The game used more memory than a server allows — usually something that keeps growing, like objects that are never destroyed.
This game isn’t responding Code on the player’s device is stuck, usually in a loop that never waits.

Sandbox and performance explains the limits.

Symptom Likely cause and fix
A script does nothing, and the Explorer says won’t run here It’s in the wrong place. Scripts run in Workspace or ServerScriptService; LocalScripts in StarterPlayerScripts or a ScreenGui in StarterGui.
A script does nothing, with no error It’s probably waiting forever in waitForChild for a misspelled name. Add a timeout.
Workspace has no child 'Door' (expected a Part) The name or place doesn’t match the Explorer — or the object doesn’t exist yet. Check the tree; on a player’s device, wait for it.
The editor says Cannot find name 'setTimeout' (or console, document, fetch) Those don’t exist in scripts. Use task.delay, print and remote events.
A new part doesn’t appear It has no parent. Set part.parent = workspace (last).
A part won’t fall or roll Parts are anchored by default. Set anchored = false.
touched never fires It only fires on the server. Handle touches in a Script. Also check canTouch.
A change made in a LocalScript doesn’t show for other players, or gets undone Changes on a player’s device stay there. Ask the server with a RemoteEvent.
A player rubber-bands after a LocalScript changes their speed The server checks movement against its own walkSpeed. Change speed on the server.
DataStoreService is server-only; ask the server with a RemoteFunction Saved data is only available to Scripts.
Two scripts see different values of a module’s variable Each script has its own copy of every module it imports. Share state through the tree, or import everything from one script.
Part.anchored expects true or false, got string "yes" A value of the wrong type. Use the type the property needs.
Player.userId is read-only Some properties are managed by the engine and can only be read.
ServerStorage.Hit isn't visible to Ann; use ReplicatedStorage A RemoteEvent the player can’t see. Keep remotes in ReplicatedStorage.
Ann sent too many remote calls; extra calls are dropped A LocalScript fires remotes too often — every frame, say. Send only when something changes.
N log lines were dropped (over 200/s) Too much printing. Print events, not every step.
invokeServer fails with the server took too long A RemoteFunction handler took more than 5 seconds to answer.