Lua is a small, fast scripting language built to be embedded in other programs,
which is why you meet it inside games, editors and servers more often than on its
own. It has one data structure, the table, and a handful of ideas that stretch a
long way: functions as values, metatables and coroutines. The reference below is
grouped by what you are trying to do, and the filter box searches all of it at once.
Type table and everything about tables comes to you, or type 5.5 to see what the
latest release added.
Every snippet is checked against Lua 5.5, the current version from
lua.org, and was run on the 5.5.1 interpreter built from
the official source. Anything newer than 5.3 says so in the notes column, and
Where Lua runs covers the older dialects in Roblox, Neovim and
LÖVE. Names like user and nums are placeholders for your own. Coming from
another language? The Ruby,
PHP, Swift and
Dart cheat sheets are grouped the same way, so
they read side by side.
Searches the task, the command and the third column. Press / from anywhere on the page.
178 commands
Running Lua and LuaRocks
| Task | Command | Notes |
|---|---|---|
| Check which version you have | lua -v | |
| Run a script | lua script.lua | |
| Open an interactive shell | lua | Ctrl+D to leave |
| Run a script, then stay in the shell | lua -i script.lua | |
| Run one line without a file | lua -e 'print(_VERSION)' | |
| Load a module before running | lua -l inspect script.lua | |
| Check a file for syntax errors | luac -p file.lua | |
| Turn on warn() messages | lua -W script.lua | |
| Where require looks for .lua files | LUA_PATH="./lib/?.lua;;" lua app.lua | The ;; keeps the default path on the end |
| Install a package | luarocks install inspect | LuaRocks is installed separately from Lua |
| Install into your home directory | luarocks install --local penlight | |
| Let Lua find --local packages | eval "$(luarocks path)" | Add it to your shell profile |
| List installed packages | luarocks list | |
| Show what a package is | luarocks show inspect | |
| Remove a package | luarocks remove inspect |
Lua is a tiny C library plus a small standalone interpreter, so system package managers often ship an older version under a name like lua5.4. The source from lua.org builds in seconds with make.
Variables and types
| Task | Code | Notes |
|---|---|---|
| Print a line | print("Hello") | Several arguments are separated by tabs |
| Local variable | local name = "Ada" | Always use local unless you mean a global |
| Global variable | count = 0 | Any name you never declared local is global |
| Constant | local MAX <const> = 100 | Lua 5.4+. Assigning to it is a compile error |
| Declare the globals a file may set | global<const> * global config | Lua 5.5+. Any other global name is then read-only |
| Assign several at once | local a, b = 1, 2 | |
| Swap two variables | a, b = b, a | |
| Comment | -- one line | |
| Block comment | --[[ several lines ]] | |
| What type is it | type(value) | Returns a string: "nil", "number", "string", "table", "function"... |
| Integer or float | math.type(3) math.type(3.0) | "integer" and "float". Lua 5.3+ |
| Is it nil | value == nil | |
| Not equal | a ~= b | Not != |
| Default when nil or false | local name = input or "Anonymous" | |
| Pick one of two values | local label = n == 1 and "item" or "items" | Breaks if the middle value is false or nil |
| Divide | 7 / 2 | Always a float: 3.5 |
| Floor division | 7 // 2 | 3. Rounds down, so -7 // 2 is -4. Lua 5.3+ |
| Remainder | 7 % 3 | Takes the sign of the divisor: -7 % 3 is 2 |
| Raise to a power | 2 ^ 10 | Always a float: 1024.0 |
| Bitwise and, or, xor, not | a & b a | b a ~ b ~a | Lua 5.3+. Integers only |
| Shift bits | 1 << 4 256 >> 4 | |
| String to number | tonumber("42") | nil when it is not a number |
| Parse in another base | tonumber("ff", 16) | |
| Number to string | tostring(42) | |
| Float to integer, if it is whole | math.tointeger(3.0) | nil for 3.5 |
| Round down or up | math.floor(3.7) math.ceil(3.2) | |
| Largest and smallest | math.max(1, 5, 3) math.min(2, 8) | |
| Random whole number from 1 to 6 | math.random(1, 6) | |
| Biggest integer, infinity | math.maxinteger math.huge | Integers wrap around on overflow |
Only nil and false are falsy. 0 and the empty string are true, which catches out anyone coming from JavaScript, Python or C. There is no separate integer type in older Lua: 5.1, LuaJIT and Luau have one number type, a double.
Strings
Every string function lives in the string table, and strings look their methods up there, so s:upper() and string.upper(s) are the same call. Positions start at 1 and ranges include both ends. Negative positions count from the end.
| Task | Code | Notes |
|---|---|---|
| Join strings | first .. " " .. last | Numbers join too: 10 .. "" is "10" |
| Length in bytes | #s | Bytes, not characters: #"café" is 5 |
| Length in characters | utf8.len("café") | 4. Lua 5.3+ |
| Change case | s:upper() s:lower() | |
| Part of a string | s:sub(1, 5) s:sub(-3) | From position 1 to 5 inclusive, and the last 3 |
| Find plain text | s:find("cat", 1, true) | Returns start and end, or nil. The true turns patterns off |
| Find a pattern | s:find("%d+") | |
| Pull out a match | s:match("%d+") | The matched text, or nil |
| Pull out several parts | local y, m, d = date:match("(%d+)-(%d+)-(%d+)") | One value per capture |
| Replace every match | s:gsub("cat", "dog") | Returns the new string and a count |
| Replace the first match only | s:gsub("cat", "dog", 1) | |
| Reuse a capture in the replacement | s:gsub("(%w+)=(%w+)", "%2=%1") | |
| Loop over every match | for word in s:gmatch("%a+") do ... end | |
| Split on commas | for part in s:gmatch("[^,]+") do ... end | Skips empty fields. Use "([^,]*)" to keep them |
| Trim whitespace | s:match("^%s*(.-)%s*$") | |
| Does the whole string match | s:match("^%d+$") ~= nil | |
| Format with placeholders | string.format("%s has %d items", name, n) | |
| Format a decimal | string.format("%.2f", price) | |
| Pad with zeros | string.format("%03d", 7) | 007 |
| Quote a string safely | string.format("%q", s) | Output can be read back by Lua |
| Repeat | s:rep(3) s:rep(3, ", ") | The second form puts a separator between copies |
| Reverse | s:reverse() | |
| Character codes | s:byte(1) string.char(72, 105) | |
| String across several lines | local text = [[ ... ]] | No escapes are processed inside |
| Newline and tab | "line\n" "col\t" | |
| Build a long string in a loop | parts[#parts + 1] = piece table.concat(parts, ", ") | Much faster than .. in a loop |
Lua patterns are not regular expressions. %d is a digit, %a a letter, %w a letter or digit, %s whitespace, %u upper case, and . any character. - is the lazy version of *. There is no | for alternatives and no {n} counts. Escape the magic characters ( ) . % + - * ? [ ] ^ $ with %.
Tables as arrays
The table is Lua's only data structure. An array is a table whose keys are 1, 2, 3 and so on, with no gaps. Every function here assumes that shape.
| Task | Code | Notes |
|---|---|---|
| Create an array | local nums = {10, 20, 30} | |
| Read by position | nums[1] nums[#nums] | Indexes start at 1. nums[0] is nil |
| How many items | #nums | Only reliable with no nil gaps |
| Append | nums[#nums + 1] = 40 | Or table.insert(nums, 40) |
| Insert at a position | table.insert(nums, 1, 5) | Shifts the rest up |
| Remove the last item | table.remove(nums) | Returns the removed item |
| Remove at a position | table.remove(nums, 1) | Shifts the rest down |
| Loop with the position | for i, v in ipairs(nums) do ... end | Stops at the first nil |
| Loop backwards | for i = #nums, 1, -1 do ... end | Safe for removing while you loop |
| Sort | table.sort(nums) | In place, returns nothing |
| Sort with your own order | table.sort(people, function(a, b) return a.age < b.age end) | Return true when a goes first |
| Join into a string | table.concat(words, ", ") | |
| Spread into arguments | math.max(table.unpack(nums)) | |
| Pack arguments with a count | local args = table.pack(...) | args.n counts nils too |
| Copy an array | local copy = table.move(nums, 1, #nums, 1, {}) | |
| Preallocate a big array | local t = table.create(1000) | Lua 5.5+. Still empty, just sized |
| Array of arrays | local grid = {{1, 2}, {3, 4}} grid[2][1] | |
| Find an item's position | for i, v in ipairs(list) do if v == x then return i end end | No built-in indexOf |
Tables as maps
The same table type works as a dictionary. Keys can be any value except nil and NaN, and a key set to nil is removed.
| Task | Code | Notes |
|---|---|---|
| Create with string keys | local user = {name = "Ada", age = 36} | |
| Key that is not a plain name | {["first name"] = "Ada", [42] = "answer"} | |
| Read a value | user.name user["name"] | nil when the key is missing |
| Read with a variable key | user[key] | user.key means the literal key "key" |
| Set a value | user.email = "ada@example.com" | |
| Remove a key | user.age = nil | |
| Loop over pairs | for key, value in pairs(user) do ... end | Order is not defined |
| Loop in key order | local keys = {} for k in pairs(t) do keys[#keys + 1] = k end table.sort(keys) | |
| Is the table empty | next(t) == nil | #t is 0 for any table with no array part |
| Count the keys | local n = 0 for _ in pairs(t) do n = n + 1 end | No built-in count |
| Read a nested value safely | local host = cfg.db and cfg.db.host | No ?. operator |
| Count how often each value appears | counts[w] = (counts[w] or 0) + 1 | |
| Use as a set | seen[item] = true if seen[item] then ... end | |
| Fill in defaults | for k, v in pairs(defaults) do if opts[k] == nil then opts[k] = v end end | |
| Shallow copy | local copy = {} for k, v in pairs(t) do copy[k] = v end | |
| Compare two tables | a == b | Only true for the same table, not equal contents |
Control flow
| Task | Code | Notes |
|---|---|---|
| If, elseif, else | if a then ... elseif b then ... else ... end | elseif is one word |
| And, or, not | a and b a or b not a | Return one of their operands, not just true or false |
| While loop | while i < 10 do ... end | |
| Loop at least once | repeat ... until done | until can see locals from inside the loop |
| Count up | for i = 1, 10 do ... end | Both ends included |
| Count in steps | for i = 10, 1, -1 do ... end | |
| Loop over an array | for i, v in ipairs(list) do ... end | |
| Loop over a table | for k, v in pairs(t) do ... end | |
| Leave a loop | break | |
| Skip to the next iteration | goto continue ::continue:: | No continue keyword. Put the label at the end of the loop body |
| Change the loop variable | local i = i | Lua 5.5 makes the for variable read-only, so shadow it first |
| Return early | if not user then return end | |
| Return from the middle of a block | do return end | return must be the last statement in a block |
Functions and closures
| Task | Code | Notes |
|---|---|---|
| Define a function | local function add(a, b) return a + b end | |
| Anonymous function | local square = function(x) return x * x end | |
| Return several values | return min, max | local lo, hi = bounds(list) |
| Keep only the first result | (bounds(list)) | Extra parentheses cut it to one value |
| Default parameter | name = name or "world" | Missing arguments arrive as nil |
| Any number of arguments | local function sum(...) end | |
| Arguments as a table | local args = {...} | |
| Count arguments, nils included | select("#", ...) | |
| Name the extra arguments | local function log(...args) print(args.n) end | Lua 5.5+. args is read-only |
| Pass arguments straight on | string.format(fmt, ...) | |
| Named arguments | connect{host = "db", port = 5432} | One table argument, no parentheses needed |
| Closure with private state | local function counter() local n = 0 return function() n = n + 1 return n end end | |
| Method call | obj:move(1, 2) | Same as obj.move(obj, 1, 2) |
| Define a method | function Point:move(dx, dy) self.x = self.x + dx end | The colon adds a self parameter |
| Recursive local function | local function fact(n) if n <= 1 then return 1 end return n * fact(n - 1) end | local function lets it see its own name |
| Pass a function as a value | table.sort(list, compare) |
Errors
| Task | Code | Notes |
|---|---|---|
| Raise an error | error("something broke") | Adds the file and line to the message |
| Blame the caller's line | error("expected a number", 2) | |
| Raise without a position | error("bad input", 0) | |
| Raise a table | error({code = 404}) | Handlers get the table back unchanged |
| Check a condition | assert(file, "config missing") | Returns its arguments when they pass |
| Catch an error | local ok, err = pcall(risky, arg1, arg2) | ok is false and err is the message |
| Catch with a traceback | xpcall(risky, debug.traceback) | |
| Function that can fail softly | return nil, "not found" | The usual style in the standard library |
| Handle a soft failure | local f, err = io.open(path) if not f then print(err) end | |
| Clean up however the block ends | local f <close> = assert(io.open(path)) | Lua 5.4+. Needs a __close metamethod, which files have |
Lua has no try and catch. pcall runs a function in protected mode and turns an error into a return value. Library functions that can fail for ordinary reasons, like io.open, return nil and a message instead of raising.
Metatables and objects
A metatable is a table of hooks that changes how another table behaves: what happens when a key is missing, what + means, how it prints. Classes in Lua are built from the __index hook.
| Task | Code | Notes |
|---|---|---|
| Attach a metatable | setmetatable(t, mt) | Returns t |
| Read a metatable | getmetatable(t) | |
| Fall back to another table | setmetatable(t, {__index = defaults}) | Only for keys missing from t |
| Compute missing values | __index = function(t, key) return ... end | |
| Intercept new keys | __newindex = function(t, key, value) rawset(t, key, value) end | Only runs for keys not already in t |
| Skip the metatable | rawget(t, k) rawset(t, k, v) rawequal(a, b) | |
| Operators | __add __sub __mul __div __mod __unm __concat | a + b calls __add(a, b) |
| Comparisons | __eq __lt __le | |
| What tostring and print show | __tostring = function(v) return "(" .. v.x .. ")" end | |
| What # returns | __len = function(v) return v.size end | |
| Call a table like a function | __call = function(self, arg) end | |
| Run code when a variable goes out of scope | __close = function(v, err) end | Lua 5.4+. For local x <close> |
| Weak keys, for a cache | setmetatable(cache, {__mode = "k"}) | "v" for weak values |
| Stop changes to the metatable | __metatable = "locked" | |
| Class with a constructor | local Point = {} Point.__index = Point | Then setmetatable({x = x}, Point) in Point.new |
| Inherit from a class | local Cat = setmetatable({}, {__index = Animal}) Cat.__index = Cat |
Modules
| Task | Code | Notes |
|---|---|---|
| Load a module | local json = require("json") | Runs json.lua once, then returns the cached result |
| Load from a subfolder | require("app.util") | Finds app/util.lua |
| Write a module | local M = {} function M.hello() end return M | |
| Keep a helper private | local function helper() end | Only what you put in the returned table is public |
| Where require looks | package.path | |
| Add your own folder | package.path = "./lib/?.lua;" .. package.path | |
| Force a reload | package.loaded.util = nil | Then require again |
| Try a module that may be missing | local ok, lib = pcall(require, "lfs") | |
| Run a file every time | dofile("config.lua") | No caching, unlike require |
| Compile a string of code | local fn = load("return 1 + 1") | Returns nil and an error on bad syntax |
| Run code in a sandbox | load(code, "user", "t", {print = print}) | The table is the only global scope it can see |
Coroutines
A coroutine is a function you can pause with yield and continue with resume. Only one runs at a time, so there are no threads or locks: it is how game scripts wait across frames and how generators keep their place.
| Task | Code | Notes |
|---|---|---|
| Create a coroutine | local co = coroutine.create(function(a) ... end) | It does not start yet |
| Start or continue it | local ok, value = coroutine.resume(co, 1) | Arguments go in, yielded values come out |
| Pause and hand back a value | local reply = coroutine.yield(value) | reply is what the next resume passes in |
| Is it running, paused or done | coroutine.status(co) | "suspended", "running", "normal" or "dead" |
| Wrap it as a plain function | local next_id = coroutine.wrap(function() ... end) | Errors are raised instead of returned |
| Use it as a for-loop iterator | for n in coroutine.wrap(gen) do ... end | The loop ends when it returns |
| Is the current code inside a coroutine | coroutine.isyieldable() | |
| Close a paused coroutine | coroutine.close(co) | Lua 5.4+. Runs pending __close handlers |
resume never raises. If the coroutine errors, resume returns false and the message, and the coroutine is dead from then on.
A class, start to finish
Most of the metatable reference in one place: a constructor, methods with :,
__tostring and __lt, a method that fails softly with nil, message, and a
subclass that inherits through __index.
local Account = {}
Account.__index = Account
function Account.new(owner, balance)
local self = setmetatable({}, Account)
self.owner = owner
self.balance = balance or 0
return self
end
function Account:deposit(amount)
if amount <= 0 then
error("deposit must be positive", 2)
end
self.balance = self.balance + amount
return self
end
function Account:withdraw(amount)
if amount > self.balance then
return nil, "insufficient funds"
end
self.balance = self.balance - amount
return self.balance
end
Account.__tostring = function(a)
return string.format("%s: %.2f", a.owner, a.balance)
end
Account.__lt = function(a, b) return a.balance < b.balance end
-- A subclass: look up missing methods on Account.
local Savings = setmetatable({}, { __index = Account })
Savings.__index = Savings
Savings.__tostring = Account.__tostring
Savings.__lt = Account.__lt
function Savings.new(owner, balance, rate)
local self = Account.new(owner, balance)
self.rate = rate
return setmetatable(self, Savings)
end
function Savings:add_interest()
return self:deposit(self.balance * self.rate)
end
local ada = Account.new("Ada", 100):deposit(50)
local alan = Savings.new("Alan", 200, 0.05):add_interest()
print(ada) --> Ada: 150.00
print(alan) --> Alan: 210.00
print(ada < alan) --> true
print(ada:withdraw(500)) --> nil insufficient funds
print(pcall(ada.deposit, ada, -5)) --> false deposit must be positiveMetamethods such as __tostring and __lt are looked up on the metatable itself,
not through __index, which is why Savings copies them from Account. Methods
like deposit are ordinary keys, so they are found through the __index chain.
A module, start to finish
A module is a file that returns a table. require finds it on package.path, runs
it once and caches the result, so every file that requires it shares the same table.
-- greeter.lua
local M = {}
local default_name = "world" -- private: not in M
function M.hello(name)
return "Hello, " .. (name or default_name)
end
function M.shout(name)
return M.hello(name):upper() .. "!"
end
return M-- main.lua, in the same folder
local greeter = require("greeter")
print(greeter.hello()) --> Hello, world
print(greeter.shout("Ada")) --> HELLO, ADA!require("greeter.util") turns the dots into folder separators and looks for
greeter/util.lua. Packages from LuaRocks land on the same search path, so they
load the same way. Commit your own modules with the rest of the project; the
Git cheat sheet covers that workflow.
Patterns, start to finish
Lua's patterns are smaller than regular expressions but cover most text jobs.
Here they pick apart a log: captures in gmatch, a nested gmatch for key=value
pairs, and the two ways to match a literal $ or ..
local log = [[
2026-09-26 12:01:07 INFO user=ada action=login
2026-09-26 12:03:44 ERROR user=alan action=upload size=12MB
2026-09-26 12:04:02 INFO user=ada action=logout
]]
local counts = {}
for date, time, level, rest in log:gmatch("(%d+%-%d+%-%d+) (%d+:%d+:%d+) (%u+)%s+([^\n]*)") do
counts[level] = (counts[level] or 0) + 1
-- Pull key=value pairs out of the rest of the line into a table.
local fields = {}
for key, value in rest:gmatch("(%w+)=(%w+)") do
fields[key] = value
end
if level == "ERROR" then
print(time, fields.user, fields.action) --> 12:03:44 alan upload
end
end
print(counts.INFO, counts.ERROR) --> 2 1
-- Escape the magic characters ( ) . % + - * ? [ ] ^ $ with %.
local price = "Total: $4.50"
print(price:match("%$(%d+%.%d+)")) --> 4.50
-- Or search for plain text with the fourth argument to find.
print(price:find("$4.", 1, true)) --> 8 10Coroutines, start to finish
A coroutine keeps its local variables and its place between calls. Wrapped with
coroutine.wrap, it becomes an iterator you can hand straight to a for loop,
and iterators can be chained into a pipeline.
-- A generator: yields values one at a time, keeps its place between calls.
local function range(from, to, step)
return coroutine.wrap(function()
for i = from, to, step or 1 do
coroutine.yield(i)
end
end)
end
for n in range(10, 30, 10) do
print(n) --> 10, 20, 30
end
-- A pipeline: each stage pulls from the one before it.
local function lines(text)
return coroutine.wrap(function()
for line in text:gmatch("[^\n]+") do
coroutine.yield(line)
end
end)
end
local function non_blank(source)
return coroutine.wrap(function()
for line in source do
if line:find("%S") then
coroutine.yield(line)
end
end
end)
end
for line in non_blank(lines("alpha\n \nbeta\n")) do
print(line) --> alpha, beta
end
-- Passing values both ways with resume and yield.
local acc = coroutine.create(function(first)
local total = first
while true do
local n = coroutine.yield(total)
total = total + n
end
end)
print(coroutine.resume(acc, 5)) --> true 5
print(coroutine.resume(acc, 10)) --> true 15
print(coroutine.status(acc)) --> suspendedDeclaring globals in Lua 5.5
A misspelt variable name in Lua silently creates a new global. Lua 5.5 adds the
global keyword to stop that. Once a file declares any global, every name it uses
must be declared, and global<const> * covers the built-ins such as print
and string as read-only.
-- Lua 5.5: from here on, every name must be declared.
-- Existing globals such as print and string can be read but not reassigned.
global<const> *
-- Globals this file is allowed to set.
global config, VERSION
VERSION = "1.2.0"
config = { debug = false }
local function load_settings()
cofnig = { debug = true } -- typo: compile error, not a silent new global
endThe typo is caught when the file is compiled, before anything runs:
attempt to assign to const variable 'cofnig'. Without the global<const> * line,
global config on its own would also stop print from being found, because
nothing declared it. None of this exists before 5.5, so leave it out of code that
has to run in Neovim, LÖVE or Roblox.
Where Lua runs
Most people write Lua inside another program, and most of those programs run an older or modified Lua. The table below is the short version. Everything on this page about tables, functions, closures, metatables and coroutines works in all of them. The version notes in the reference tell you what does not.
| Where | Language | What is different from Lua 5.5 |
|---|---|---|
| Roblox | Luau, derived from Lua 5.1 | Type annotations, continue, +=, backtick strings, const x = 1. No goto, no integers, no & or | (use bit32) |
| Neovim | LuaJIT, Lua 5.1 plus some extras | No //, no bitwise operators (use bit), no integers, no utf8, no <const> |
| LÖVE (games) | LuaJIT by default | Same as Neovim |
| Redis scripts, World of Warcraft add-ons | Lua 5.1 | Same limits as LuaJIT, plus no goto |
Roblox
Roblox games are scripted in Luau, which Roblox develops from
Lua 5.1. This script gives each player a coin counter on the leaderboard and tops
it up every minute. It is Luau, not Lua 5.5: the : Player annotation, the backtick
string, += and task.wait do not exist in standard Lua.
-- A Script in ServerScriptService. Luau, not Lua 5.5.
local Players = game:GetService("Players")
local STARTING_COINS = 100
Players.PlayerAdded:Connect(function(player: Player)
local stats = Instance.new("Folder")
stats.Name = "leaderstats"
stats.Parent = player
local coins = Instance.new("IntValue")
coins.Name = "Coins"
coins.Value = STARTING_COINS
coins.Parent = stats
print(`{player.Name} joined with {coins.Value} coins`)
end)
while true do
task.wait(60)
for _, player in Players:GetPlayers() do
local stats = player:FindFirstChild("leaderstats")
local coins = stats and stats:FindFirstChild("Coins")
if coins then
coins.Value += 10
end
end
endLuau numbers are always floats, so 10 / 2 prints 5 rather than 5.0, and
table.create(3, "x") fills a new array with three copies of "x", where Lua 5.5's
table.create only reserves space.
Neovim
Neovim reads ~/.config/nvim/init.lua at startup and runs it with LuaJIT, so the
Lua 5.1 rules apply. The editor's API lives in the global vim table. This config
was loaded in Neovim 0.12; vim.hl.on_yank needs 0.11 or later.
-- ~/.config/nvim/init.lua
vim.g.mapleader = " "
vim.opt.number = true
vim.opt.expandtab = true
vim.opt.shiftwidth = 2
vim.keymap.set("n", "<leader>w", "<cmd>write<cr>", { desc = "Save the file" })
vim.api.nvim_create_autocmd("TextYankPost", {
desc = "Flash the text you just yanked",
callback = function()
vim.hl.on_yank()
end,
})Game scripting with LÖVE
LÖVE is a free 2D game framework: you write main.lua,
define the callbacks it looks for, and it calls them every frame. It runs LuaJIT
by default, so the same 5.1 limits apply. Engines that embed Lua work the same way:
the host calls your functions, and your functions call the host's API.
-- main.lua for LÖVE: run the folder with `love .`
local player = { x = 100, y = 100, speed = 200 }
function love.load()
love.window.setTitle("Move the square")
end
function love.update(dt)
-- dt is the seconds since the last frame, so speed is per second.
if love.keyboard.isDown("right") then player.x = player.x + player.speed * dt end
if love.keyboard.isDown("left") then player.x = player.x - player.speed * dt end
end
function love.draw()
love.graphics.rectangle("fill", player.x, player.y, 32, 32)
endGotchas
The mistakes almost everyone makes in their first week of Lua.
| Looks right | What actually happens | Do this instead |
|---|---|---|
count = 0 inside a function | Creates or overwrites a global | local count = 0, or global declarations in 5.5 |
if n then when n is 0 | The branch runs: only nil and false are falsy | if n ~= 0 then |
if a != b then | Syntax error | a ~= b |
#{1, 2, nil, 4} | 2 or 4, depending on version and how the table was built | Keep arrays gap-free, or track the count in a field |
x = cond and false or "other" | Always "other", because false fails the or | A plain if |
s:find(".") | Finds the first character: . matches anything | s:find(".", 1, true) or s:find("%.") |
print(s:gsub("a", "b")) | Prints the string and a count | print((s:gsub("a", "b"))) |
obj.method() | self is nil inside the method | obj:method() |
for i = 1, 3 do i = i + 1 end | Compile error in 5.5: the loop variable is read-only | local j = i and change j |
s = s .. piece in a big loop | Builds a new string every time, which is slow | Collect pieces in a table, then table.concat |
print(0.1 + 0.2) | 0.30000000000000004 in 5.5, where 5.4 printed 0.3 | string.format("%.2f", x) for display |
table.sort(t, function(a, b) return a <= b end) | Can raise "invalid order function" | Use <: the function must be a strict order |
table.sort is also not stable: items that compare equal can come out in any
order. It is a quick sort variant,
unlike merge sort, which keeps equal
items in place. Both are on the site as step-through visualisations.
Common questions
Which version of Lua does this cheat sheet cover?
Lua 5.5, the current version, which came out in December 2025, checked against the 5.5.1 release. Most of the page also works in Lua 5.4. Anything newer than 5.3 says so in the notes column, for example global declarations, named varargs and table.create need 5.5, and <const> and <close> need 5.4. Run lua -v to see which version you have.
Why do Lua arrays start at 1?
Lua was designed in 1993 for engineers who were not professional programmers, and counting from 1 matched how they counted. The standard library follows it throughout: #t, ipairs, table.insert, table.sort and string positions all assume the first item is at 1. You can store something at index 0, but none of those functions will see it.
What is the difference between pairs and ipairs?
ipairs walks the array part of a table in order, from 1 up to the first nil, and gives you the index and value. pairs visits every key in the table, including string keys, in no particular order. Use ipairs for lists where order matters and pairs for dictionaries or when you need every key.
What is the difference between a dot and a colon in Lua?
A colon passes the object as a hidden first argument called self. obj:move(1, 2) is the same as obj.move(obj, 1, 2), and function Point:move(dx, dy) is the same as function Point.move(self, dx, dy). Calling a method with a dot by mistake shifts every argument along by one, which usually shows up as an error about self being a number or nil.
Is Roblox Lua the same as Lua?
No. Roblox runs Luau, a language derived from Lua 5.1 and developed by Roblox. It adds optional type annotations, string interpolation with backticks, compound assignment like +=, a continue keyword and const, and it leaves out goto, the integer subtype and the bitwise operators. Everything on this page about tables, functions, metatables and coroutines carries over, but check the version notes before copying 5.3+ syntax.
Which version of Lua does Neovim use?
Neovim embeds LuaJIT, which follows Lua 5.1 with some later additions such as goto. So in an init.lua there is no // operator, no bitwise operators (use the bit module), no integer subtype, no utf8 library and no <const> or <close>. The Neovim API lives in the global vim table, for example vim.opt, vim.keymap.set and vim.api.
Why does #t give the wrong length?
The length operator is only defined for sequences, tables with values at 1 to n and no nil in between. If there is a gap, # can return the position of any border, where a non-nil value is followed by nil, so different Lua versions and even different ways of building the same table can give different answers. Keep arrays gap-free, or store the count yourself in a field such as n.
Does Lua have classes?
Not as a keyword. A class is a table of methods that is also used as the metatable of its instances, with __index pointing back at itself, so a missing key on an instance is looked up in the class. Inheritance is the same trick one level up: the subclass gets a metatable whose __index is the parent class. The worked example on this page shows the whole pattern.
