Skip to content
AgileLogics
5 min read

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 nil and false are falsy; compare explicitly (count > 0, name ~= "").
  • Avoid holes in arrays; don't trust # on tables with nil gaps.
  • Don't rely on pairs order; 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, message for expected failures; error for bugs.
  • Wrap host callbacks and scripts in pcall/xpcall so 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.