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
Section titled “Print and warn”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 |
import { Color3, getChild, Part, print, Vector3, warn, workspace } from '@rally/sdk';
const door = workspace.findFirstChild('Door', true);print('door:', door); // door: Workspace.Map.Doorprint('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.
Where output shows up
Section titled “Where output shows up”In Studio
Section titled “In Studio”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.
In the player: the developer console
Section titled “In the player: the developer console”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 `.
Reading an error
Section titled “Reading an error”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.
Compile errors
Section titled “Compile errors”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.
Ways to find a bug
Section titled “Ways to find a bug”-
Print with labels and paths.
print('door', door, 'open?', door.getAttribute('open'))tells you far more thanprint(x). Instances print as their path, which settles “which one?”. -
Give
waitForChilda 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 onconst 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.
How Play differs from a real server
Section titled “How Play differs from a real server”Play is the quickest way to test, but because the server and your device run together, a few things behave differently there:
RunService.isServer()andRunService.isClient()are bothtrue, andPlayers.localPlayeris 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.
When a game stops
Section titled “When a game stops”| 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.
Common mistakes
Section titled “Common mistakes”| 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. |
Next steps
Section titled “Next steps”- Testing — Play and test servers in Studio.
- Tasks and timing — keep every step fast.
- Multiplayer — problems between the server and players’ devices.