Style and common mistakes
Lesson 8.5
The mistakes almost every Lua beginner makes, how to spot them, and a few habits that keep your code easy to read.
Common mistakes
Forgetting local
local function count_diamonds(list)
total = 0 -- oops: a global
for _, item in ipairs(list) do
if item == "Diamond" then total = total + 1 end
end
return total
end
print(count_diamonds({ "Diamond", "Dirt" })) --> 1
print(total) --> 1
The function works, but total has leaked out: it is now a global that any other code can overwrite. Write local total = 0. The VS Code Lua extension underlines globals, which catches this quickly.
Typos give nil, not an error
local player = { health = 20 }
print(player.helath) --> nil
Reading a missing field or variable silently gives nil. The error, if any, comes later and somewhere else, for example attempt to perform arithmetic on a nil value (field 'helath'). When a value is mysteriously nil, check the spelling first.
Counting from 0
local items = { "Sword", "Bow", "Bread" }
print(items[0]) --> nil
print(items[#items]) --> Bread
Lists start at 1, and the last item is at #t. Loops go for i = 1, #t, not for i = 0, #t - 1.
!= instead of ~=
Lua writes “not equal” as ~=. a != b is a syntax error. Similarly, !x doesn’t exist: write not x. And && and || are and and or.
+ instead of … for text
local name = "Alex"
print("Hi " .. name) --> Hi Alex
print(pcall(function() return "Hi " + name end))
The last line fails with attempt to perform arithmetic on a string value: + is only for numbers.
Dot instead of colon
local s = "stone"
print(s:upper()) --> STONE
print(pcall(s.upper)) --> false bad argument #1 to 'string.upper' (string expected, got no value)
A method defined with : needs to be called with :. If you see bad argument #1 or an error about self, check the call.
Thinking 0 or “” is false
local arrows = 0
if arrows then
print("this runs even with 0 arrows, because 0 is true")
end
if arrows > 0 then
print("this only runs when there are arrows")
end
Only false and nil are falsy.
Changing a list while walking it forwards
Removing items with table.remove inside a forward ipairs loop skips items. Walk backwards, or build a new list (see Lists).
Expecting pairs to keep order
pairs has no fixed order. When order matters, use a list with ipairs, or sort the keys first (see Dictionaries).
Modifying a table you meant to copy
local b = a doesn’t copy a table; both names point to the same one. Copy it explicitly (see Working with tables).
Floats and ==
0.1 + 0.2 == 0.3 is false. Compare floats with a small tolerance, and use integers for counts (see Numbers and maths).
Multiple returns cut short
Only the last call in a list keeps all its results. print(f(), "x") shows just the first result of f (see Functions).
Reading error messages
Most runtime errors follow a few shapes. Learn to read them and debugging becomes much faster:
| Message | Usually means |
|---|---|
attempt to index a nil value (local 'p') |
p is nil and you wrote p.something |
attempt to call a nil value (global 'pirnt') |
a function name is misspelt, or not defined yet |
attempt to perform arithmetic on a nil value (field 'x') |
a number you expected is missing |
attempt to concatenate a nil value |
one side of .. is nil |
attempt to compare number with nil |
one side of < or > is missing |
bad argument #1 to 'table.insert' (table expected, got nil) |
you passed the wrong thing to a library function |
'end' expected (to close 'if' at line 3) |
a block is missing its end |
stack overflow |
a function calls itself without stopping |
The text in brackets names the variable. Go to the line number in the message, find that name, and ask why it holds that value. Adding a print just before the line is often the fastest way to find out.
Style habits
These aren’t rules of the language, but most Lua code follows them, including the Celestial API:
- Indent each block by 2 spaces (some projects use 4; be consistent).
snake_casefor variables and functions:max_health,give_item.PascalCasefor classes:Mob,ItemStack.UPPER_CASEfor constants:MAX_STACK._for values you don’t use:for _, v in ipairs(t).- Short functions that do one thing, with names that say what they do.
- Early returns instead of deep nesting.
- Comments that explain why, not what.
-- creepers need a 1.5 s fuseis useful;-- add 1 to iis not. localeverything, and require modules into locals at the top of a file.
A function written in this style:
local MAX_STACK <const> = 64
-- How many items still fit into the stacks we already have.
local function free_space(stacks)
local free = 0
for _, stack in ipairs(stacks) do
free = free + (MAX_STACK - stack.count)
end
return free
end
print(free_space({ { count = 60 }, { count = 1 } })) --> 67
Performance tips
Lua is fast, but code that runs every tick should still be tidy:
- Locals are faster than globals and table fields. In a hot loop, copy a function you call often into a local:
local floor = math.floor. - Build strings with
table.concatinstead of..in a loop. - Reuse tables instead of creating new ones every tick when you can.
- Look things up in a dictionary instead of searching a list.
Don’t optimise before you have measured. Clear code first, then speed up the parts that turn out to be slow.
Try it
This function is supposed to return the names of all players below 10 health, sorted. It has five bugs. Find and fix them.
-- this code has mistakes on purpose
function low_health(players)
result = {}
for i = 0, #players do
local p = players[i]
if p.health < 10 then
table.insert(result, p.name)
end
end
table.sort(result, function(a, b) return a <= b end)
return results
end
Show the fixed code
local function low_health(players) -- 1. local function
local result = {} -- 2. local result
for i = 1, #players do -- 3. lists start at 1
local p = players[i]
if p.health < 10 then
table.insert(result, p.name)
end
end
table.sort(result, function(a, b) return a < b end) -- 4. strict compare
return result -- 5. the name was misspelt
end
local players = {
{ name = "Noor", health = 4 },
{ name = "Alex", health = 18 },
{ name = "Kai", health = 9 },
}
print(table.concat(low_health(players), ", ")) --> Kai, Noor
In the original, players[0] is nil, so the first round already fails with attempt to index a nil value (local 'p'). Had it got further, return results would have quietly returned nil, a typo that Lua doesn’t report. (table.sort(result) with no function would also be fine here, since names sort alphabetically by default.)
Recap: use local, count from 1, write ~= and .., call methods with :, remember that only false and nil are falsy, and read error messages from the line number.
End of part 1
You now know the whole Lua language: values, control flow, functions, tables, metatables, errors and coroutines. That is everything you need to write mods.
Part 2 of the course takes you from here to a finished Celestial mod: setting up the SDK, reacting to events, adding items and blocks, commands, screens, and publishing your mod. It starts with Setting up. To go deeper into the language itself, the Lua reference manual and the book Programming in Lua by Roberto Ierusalimschy, one of Lua’s creators, are the best places.