Skip to content

Debugging

Lesson 16.1

Every mod has bugs at first. The trick is finding them fast. This lesson shows how to read the game’s error messages, the tools that help, and the mistakes almost everybody makes once.

Reading an error

When your code fails, the log (logs/latest.log) gets a line like this:

[my_mod] error in player.join: scripts/main.lua:12: attempt to index a nil value (field 'player')
stack traceback:
  scripts/main.lua:12: in function <scripts/main.lua:10>

Read it from left to right:

  1. [my_mod]: which mod.
  2. error in player.join: what the game was doing, here running your player.join handler.
  3. scripts/main.lua:12: the file and line.
  4. The message itself: here, something called player was nil, and the code tried to read a field from it.

The traceback below lists the calls that led there, newest first, exactly like the tracebacks in Error handling.

Errors from the API

When an API call fails, the error is a table with a code and a message:

[my_mod] error in block.break: [wrong_side] cl.level_set_block failed: wrong_side

The codes you will see most:

Code Usually means
invalid_argument A wrong value, like a typo in an id or a number out of range.
not_found An item, block or option that doesn’t exist.
wrong_side A server function on the client, or the other way round.
wrong_phase Too early or too late, like registering an item after startup.
not_loaded A block in a part of the world that isn’t loaded.
permission_denied A missing capability, or another mod’s namespace.

Catching errors

pcall from part 1 works with API calls. The error’s code tells you what happened:

local ok, err = pcall(function()
  world:set_block(pos, "minecraft:stone")
end)
if not ok then
  cl.log.warn("could not place the block: %s", tostring(err))
end

cl.try is a shortcut for single calls. cl.try.<module>.<function>(...) returns true and the results, or false and the error, instead of raising it:

local ok, state = cl.try.level.get_block(world, pos)
if ok then
  cl.log("block state", state)
elseif state.code == "not_loaded" then
  cl.log("that part of the world isn't loaded")
end

Looking inside values

cl.inspect(value) turns a table into readable text. Use it with cl.log when you aren’t sure what a value holds:

cl.log(cl.inspect({ name = "ruby", count = 3, tags = { "gem", "red" } }))
-- { count = 3, name = "ruby", tags = { 1 = "gem", 2 = "red" } }

Positions print nicely on their own: cl.log(event.pos) writes (12, 64, -30).

When the mod is turned off

If a mod causes 10 errors within one minute, the game turns it off for the rest of the session, so a broken mod can’t flood the log or slow the game. The log says:

[my_mod] disabled for this session: 10 errors within one minute (last: ...)

and the Mods screen shows the reason. Fix the bug and restart the game. An error in a tick or render handler reaches 10 quickly, because those run so often.

Common mistakes

attempt to index a nil value (field 'player'). The event has no player: the block was broken by an explosion, or the entity isn’t a player. Check first: if not event.player then return end.

attempt to index a nil value (local 'p') in a command. ctx:player() is nil when the command came from the console or a command block.

give: unknown item 'minecraft:diamon'. A typo in an id. Ids are lowercase, with the namespace in front.

registries are frozen; my_mod:ruby needs a restart. Items and blocks must be registered in lifecycle.register. If you added one while the game was running, restart.

event used after its callback returned. The code used event after the handler had returned, usually inside cl.spawn after a sleep. Copy the fields into locals before waiting.

cl.sleep_ticks must be called inside cl.spawn. Wrap the waiting code in cl.spawn(function() ... end).

[wrong_side] ... failed: wrong_side. A client function on the server or the other way round. Client code belongs in lifecycle.client_setup and client events.

cl.on: unknown event 'player.joined' (a warning, not an error). A typo in an event name. Your handler never runs.

Nothing happens at all. Is the mod loaded? Look for its loaded line in the log, or open the Mods screen. Does the event fire where you think it does (server or client)? Put a cl.log at the top of the handler to see whether it runs.

hot reload failed, keeping the old code. The changed file has an error, usually a syntax error such as a missing end. The old code keeps running until you save a working version.

Keeping mods fast

Players notice lag before they notice bugs. A few habits keep a mod fast:

  • Do heavy work in tick events, not in render, and not every tick if every second is enough.
  • Unsubscribe from events you no longer need (sub:cancel()).
  • Spread big jobs over several ticks with cl.spawn and cl.sleep_ticks(1).
  • cl.heap_bytes() tells you how much memory your mod’s Lua uses. Log it now and then while developing; it should go up and down, not only up.

Try it

This handler should give every new player a map, but it fails. Find both bugs.

cl.on("player.join", function(e)
  local p = event.player
  p:give("minecraft:maps")
end)
Show a solution
  1. The parameter is called e, but the code uses event, which is nil. The log says attempt to index a nil value (global 'event').
  2. The item id is wrong: it is "minecraft:map". After fixing the first bug, the log would say give: unknown item 'minecraft:maps'.
cl.on("player.join", function(event)
  event.player:give("minecraft:map")
end)

Fixing one error often reveals the next. That is normal: fix, save, try again.

Recap: read the log line from left to right, use pcall, cl.try and cl.inspect to dig in, and check for nil players and typos first.