When nothing happens#
Something is not working. This page is the order to check things in.
Where messages show up#
print and every error go to the script console of whoever owns the object, which is you while you are building.
local self = require("self") self:on_touch(function(player) print("touched by " .. player.name)end)That console is for finding out what your script is doing. It is not how you talk to players. For that, see Menus and HUDs.
You get up to 32 lines per event and 512 bytes per line. A print inside a fast timer will be trimmed, and noticing that is itself useful information.
The three kinds of not working#
1. Nothing happens at all#
Either the script did not load, or the handler was never registered.
Save the script and watch the console. A script that fails while loading says so right then, and none of its handlers exist afterwards. So the symptom is silence later, not an error at the moment you click.
Three questions, in this order:
- Is the script attached to the object you are actually clicking?
- Did you save?
- Was there an error at the moment you saved?
2. It worked once and then stopped#
The script was killed. Look for one of these:
| Message | What happened |
|---|---|
script budget exceeded | one event used more than 50,000 instruction units |
script memory limit exceeded | it went past 4 MB |
Both mean a loop with no way out. The script will not run again until you save it.
local self = require("self") -- kills the scriptwhile true do self:rotate_by(0, 1, 0)endlocal self = require("self") -- what you meantself:on_timer(0.05, function() self:rotate_by(0, 1, 0)end)3. It half works#
An ordinary error threw away that one event. The script is still alive and the next click runs normally. The message names the line.
The messages you will actually see#
attempt to index nil#
You read a field of something that is not there.
local self = require("self") local p = self:part("hinge")p:rotate_by(0, 5, 0) -- if there is no "hinge", p is nilThe fix always has the same shape. Check, leave early, carry on:
local self = require("self") local p = self:part("hinge")if not p then print("no part called 'hinge'") returnendp:rotate_by(0, 5, 0)This is by far the most common script error. Anything that can answer nil deserves the check: self:part, players:get, objects:get, self.store:get, self.contents:get, world:raycast.
attempt to call a nil value#
You called something that is not a function. It is almost always one of three things.
A dot where a colon belongs. self.move_to(0, 5, 0) instead of self:move_to(0, 5, 0).
A typo in the name. self:rotateby instead of self:rotate_by.
A method that does not exist. Check it in the reference rather than guessing. A plausible name is not the same as a real one.
attempt to perform arithmetic, or to concatenate#
You mixed text and numbers.
print("coins: " .. 5) -- fineprint("coins: " .. nil) -- errorlocal n = "5" + 1 -- errorself.store always hands back text, so this bites there most of all:
local self = require("self") local n = tonumber(self.store:get("coins") or "0") or 0print(n)unknown module#
require with a name that is not built in and is not one of your modules.
The built-ins are self, world, players, events, objects, vec, json, http and task.
For your own module, attach a sibling script with that name to the same object, or put it on the object named Modules and require "@modules/<name>". The script that asked restarts on its own once the module is saved. See Sharing code.
cyclic require of module#
Two of your modules require each other. One of them has to stop.
When there is no error at all#
This is the hard case. Nothing is red, and nothing happens.
Look at the second return value#
Most verbs refuse quietly unless you look.
local self = require("self")local players = require("players") local p = players:list()[1]if p then local ok, why = self:hold({ player = p.id }) if not ok then print("hold refused: " .. tostring(why)) endendIf a call seems to do nothing, print its second return value. The reason is almost always sitting right there, and every one of them is listed in Limits and refusals.
Count your effects#
Eight world effects per event. The ninth quietly does not happen.
local world = require("world") for i = 1, 20 do world:spawn({ shape = "sphere" }) -- only 8 of these existendCheck you are not writing the same thing over and over#
A timer that writes properties every tick, whether or not anything changed, spends the region's budget for nothing and can crowd out the writes that matter. Act when the answer changes:
if should ~= lit then lit = should set_all(lit)endCheck whose screen it is#
A verb that draws on a player refuses when it is aimed at somebody else's screen from the wrong kind of script, and answers nil plus a reason. Print it.
Working out where you are#
When you cannot tell which branch ran, print the branch rather than the value.
local self = require("self") self:on_touch(function(p) print("touch: entered") if self:held_by() then print("touch: already held, leaving") return end print("touch: holding") local ok, why = self:hold({ player = p.id }) print("touch: hold said " .. tostring(ok) .. " " .. tostring(why))end)It is ugly, and it finds the problem in one click. Take the lines out afterwards.
Two reads and nothing found means stop reading
A defect of order, such as two things writing the same value in one frame, or a handler running before the thing it needs exists, is invisible on the page and obvious in a log. One printed line per event beats a third read-through.
Next#
- Limits and refusals explains every reason
- Trying things alone is how to break things safely
Updated 26 September 2026