Skip to content

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_case for variables and functions: max_health, give_item.
  • PascalCase for classes: Mob, ItemStack.
  • UPPER_CASE for 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 fuse is useful; -- add 1 to i is not.
  • local everything, 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.concat instead 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.