Best Practices for Production Lua
Prerequisites: Testing and debugging
A checklist that pulls the course together — use it to review Lua code before it ships in a game, plugin, or server.
Structure
- Everything
local; modules return a table and never create globals. - One module per file; small functions; early returns instead of deep nesting.
- Classes via the metatable pattern (or one class library, used consistently).
- Know your target: Lua 5.4, LuaJIT, or Luau — and avoid features it lacks.
Correctness
- Remember only
nilandfalseare falsy; compare explicitly (count > 0,name ~= ""). - Avoid holes in arrays; don't trust
#on tables withnilgaps. - Don't rely on
pairsorder; sort keys when order matters. - Use
:for method calls — the most common Lua bug. - Validate function arguments at module boundaries (
assert,type).
Errors and resources
- Return
nil, messagefor expected failures;errorfor bugs. - Wrap host callbacks and scripts in
pcall/xpcallso one error doesn't crash the game or server; log tracebacks. - Close files and connections explicitly (or with
<close>).
Security
- Load untrusted code only with
load(code, name, "t", sandboxEnv), with instruction and memory limits. - Never load untrusted bytecode.
- On servers, validate all client input; in Roblox, never trust the client.
Performance and quality
- Profile before optimizing; cache hot globals in locals; build strings
with
table.concat; reuse tables in game loops. - Format with StyLua, lint with luacheck, annotate types for lua-language-server, and test with busted in CI.
A final example
---@class Inventory
local Inventory = {}
Inventory.__index = Inventory
---@return Inventory
function Inventory.new()
return setmetatable({items = {}, count = 0}, Inventory)
end
---@param name string
---@param qty integer?
function Inventory:add(name, qty)
assert(type(name) == "string" and name ~= "", "name must be a non-empty string")
qty = qty or 1
assert(math.type(qty) == "integer" and qty > 0, "qty must be a positive integer")
self.items[name] = (self.items[name] or 0) + qty
self.count = self.count + qty
end
---@param name string
---@return boolean ok, string? err
function Inventory:remove(name)
local have = self.items[name]
if not have then return false, "no " .. name .. " in inventory" end
self.items[name] = have > 1 and have - 1 or nil
self.count = self.count - 1
return true
end
function Inventory:list()
local names = {}
for name, qty in pairs(self.items) do names[#names + 1] = name .. " x" .. qty end
table.sort(names)
return table.concat(names, ", ")
end
local inv = Inventory.new()
inv:add("potion", 3)
inv:add("sword")
print(inv:remove("potion"))
print(inv:remove("shield"))
print(inv:list(), inv.count)
print(pcall(inv.add, inv, "", 1))
true
false no shield in inventory
potion x2, sword x1 3
false main.lua:13: name must be a non-empty string
Annotations, validation, nil, message errors, sorted output, and a clean
class — the habits that make Lua code reliable.
What's next
Try the Advanced Projects, then read What You Should Know Now — and What's Next for where to go from here.