Control Flow and Functions
Lua’s control flow is small and old-fashioned: no continue, no switch, no ternary operator, and every block ends with end. Functions, on the other hand, are unusually capable — multiple return values and closures are first-class parts of the design, not conveniences.
Conditionals
Section titled “Conditionals”local n = 7
if n < 0 then print("negative")elseif n == 0 then print("zero")else print("positive")endthen and end are mandatory; there is no single-statement form and no braces. Any value works as a condition — only nil and false are false.
There is no switch/match. The idiomatic replacement is a table of functions:
local handlers = { add = function(a, b) return a + b end, sub = function(a, b) return a - b end,}
local function dispatch(op, a, b) local h = handlers[op] or error("unknown op: " .. tostring(op)) return h(a, b)end
print(dispatch("add", 2, 3)) -- => 5local i = 1while i <= 3 do print(i) i = i + 1endrepeat … until
Section titled “repeat … until”Runs the body at least once, then tests. The condition is inside the body’s scope, which is unique to Lua and genuinely useful:
local linerepeat line = io.read("l") local trimmed = line and line:match("^%s*(.-)%s*$")until trimmed == nil or trimmed ~= "" -- `trimmed` is visible hereNote the loop exits when the condition is true (until, not while).
Numeric for
Section titled “Numeric for”for i = 1, 5 do io.write(i, " ") end -- => 1 2 3 4 5for i = 10, 1, -2 do io.write(i, " ") end -- => 10 8 6 4 2for x = 0, 1, 0.25 do print(x) end -- => 0.0, 0.25, 0.5, 0.75, 1.0The form is for var = start, limit, step. The limit is inclusive, step defaults to 1, and a step of 0 raises an error. All three expressions are evaluated exactly once, before the loop starts — changing the limit variable inside the loop has no effect.
The control variable is a fresh local on each iteration. Assigning to it inside the body does not affect the next iteration, and closures created in the loop each capture their own copy:
local fns = {}for i = 1, 3 do fns[i] = function() return i end endprint(fns[1](), fns[2](), fns[3]()) -- => 1 2 3Generic for
Section titled “Generic for”for k, v in pairs(t) do ... endfor i, v in ipairs(t) do ... endfor word in ("a b c"):gmatch("%S+") do print(word) endThe expression after in produces up to three values: an iterator function, a state, and an initial control value. Each iteration calls iterator(state, control); the first returned value becomes the new control value, and the loop stops when it is nil.
pairs(t) simply returns next, t, nil. Knowing this, you can write iterators as plain functions:
-- Stateless iterator: no closure allocated per loop.local function range_iter(limit, i) i = i + 1 if i <= limit then return i, i * i endend
local function range(n) return range_iter, n, 0end
for i, sq in range(4) do io.write(i, "=", sq, " ") end-- => 1=1 2=4 3=9 4=16Or as closures, which is easier to read and allocates one table/upvalue set per loop:
local function chars(s) local i = 0 return function() i = i + 1 if i <= #s then return i, s:sub(i, i) end endend
for i, c in chars("lua") do print(i, c) endipairs vs pairs is covered in depth on the Tables page — the short version is that ipairs walks 1, 2, 3, … and stops at the first nil, while pairs visits every key in unspecified order.
break and goto
Section titled “break and goto”break exits the innermost loop. In Lua 5.1 it had to be the last statement in a block (people wrote do break end to work around it); 5.2+ removed that restriction.
Lua has no continue. Since 5.2 (and in LuaJIT) the idiom is goto:
for i = 1, 10 do if i % 2 == 0 then goto continue end print(i) ::continue::endgoto can only jump to a label in the same or an enclosing block, and never into the scope of a local variable. That makes it safe: you can jump forward out of nested blocks or backward to the top of a loop, but you cannot jump into the middle of one.
-- Breaking out of nested loops in one jump:local found_row, found_colfor i = 1, #grid do for j = 1, #grid[i] do if grid[i][j] == target then found_row, found_col = i, j goto done end endend::done::print(found_row, found_col)Operators
Section titled “Operators”| Category | Operators | Notes |
|---|---|---|
| Arithmetic | + - * / // % ^ |
/ and ^ always produce floats; // is floor division (5.3+) |
| Bitwise (5.3+) | & | ~ << >> and unary ~ |
64-bit integers only |
| Comparison | == ~= < > <= >= |
~= is “not equal” — there is no != |
| Logical | and or not |
Short-circuiting; and/or return operands, not booleans |
| Concat | .. |
Strings and numbers; right-associative |
| Length | # |
Bytes for strings, a border for tables |
Precedence, lowest to highest:
orand< > <= >= ~= ==|~&<< >>.. (right-associative)+ -* / // %not # - ~ (unary)^ (right-associative)Two consequences worth memorizing: .. binds tighter than comparison but looser than +, so "n=" .. 1 + 2 is "n=3"; and ^ is right-associative and binds tighter than unary minus, so -2^2 is -4.0, not 4.
and / or as expressions
Section titled “and / or as expressions”a and b evaluates a; if it is falsy it returns a, otherwise it evaluates and returns b.
a or b evaluates a; if it is truthy it returns a, otherwise it evaluates and returns b.
print(nil and 5) -- => nilprint(false or "x") -- => xprint(1 and 2) -- => 2This produces two idioms you will see constantly:
-- Default valueslocal function greet(name) name = name or "world" print("hello " .. name)end
-- Conditional expression (Lua's closest thing to a ternary)local label = ok and "yes" or "no"or also gives you lazy defaults for table fields:
opts = opts or {}opts.retries = opts.retries or 3Unlike in JavaScript, opts.retries or 3 does not override an explicit 0, because 0 is truthy in Lua. The pattern only misbehaves when the legitimate value is false.
Functions
Section titled “Functions”local function add(a, b) return a + bend
local mul = function(a, b) return a * b end -- same thing, no recursion by name
function Global.helper() end -- assigns into the table GlobalArguments are matched positionally. Extra arguments are discarded; missing ones are nil. There are no default parameter values and no keyword arguments.
local function f(a, b) return a, b endprint(f(1, 2, 3)) -- => 1 2print(f(1)) -- => 1 nilThe conventional stand-in for keyword arguments is a table, which has dedicated call syntax:
local function connect(opts) local host = opts.host or "localhost" local port = opts.port or 5432 return host .. ":" .. portend
print(connect{ host = "db.internal" }) -- => db.internal:5432f{...} and f"..." are sugar for f({...}) and f("...") — a single table or string argument needs no parentheses. That is why require "socket" and print [[text]] are valid.
Multiple return values
Section titled “Multiple return values”A function can return any number of values, and the call site decides how many it wants.
local function minmax(t) local lo, hi = t[1], t[1] for i = 2, #t do if t[i] < lo then lo = t[i] end if t[i] > hi then hi = t[i] end end return lo, hiend
local lo, hi = minmax{ 3, 1, 4, 1, 5 }print(lo, hi) -- => 1 5The adjustment rules are precise and worth internalizing:
- A call in the last position of an expression list expands to all its values.
- A call anywhere else is truncated to exactly one value.
- Wrapping a call in parentheses truncates it to one value.
local function three() return 1, 2, 3 end
print(three()) -- => 1 2 3 (last position)print(three(), 10) -- => 1 10 (truncated)print((three())) -- => 1 (parenthesized)
local t1 = { three() } -- {1, 2, 3}local t2 = { three(), 10 } -- {1, 10}local a, b = three(), 0 -- a=1, b=0This is also why print(#{three()}) is 3 but print(#{three(), 0}) is 2.
The standard library leans on this heavily. string.find returns start and end; pcall returns a success flag followed by results; io.open returns nil, message on failure:
local f, err = io.open("/nope", "r")if not f then print("failed: " .. err) endVarargs
Section titled “Varargs”... in a parameter list collects the remaining arguments.
local function log(level, ...) io.write("[", level, "] ") print(...)end
log("info", "user", 42) -- => [info] user 42... follows the same expansion rules as a function call. To count arguments including nils, use select("#", ...); to skip the first n, use select(n, ...):
local function count(...) return select("#", ...) endprint(count(1, nil, 3)) -- => 3 (a plain #{...} would be unreliable)
local function tail(...) return select(2, ...) endprint(tail("a", "b", "c")) -- => b cLua 5.2+ gives you table.pack / table.unpack for round-tripping:
local function safe_call(f, ...) local packed = table.pack(pcall(f, ...)) if not packed[1] then return nil, packed[2] end return table.unpack(packed, 2, packed.n)endtable.pack(...) returns a table with an n field holding the true count — the only reliable way to store a vararg list that may contain nil.
Functions are values
Section titled “Functions are values”Functions are ordinary values: store them in tables, pass them as arguments, return them.
local function map(t, fn) local out = {} for i = 1, #t do out[i] = fn(t[i]) end return outend
local doubled = map({1, 2, 3}, function(x) return x * 2 end)print(table.concat(doubled, ",")) -- => 2,4,6Closures
Section titled “Closures”An inner function captures the variables (not the values) of the enclosing scope. Captured variables are called upvalues, and they persist after the enclosing function returns.
local function counter() local n = 0 return function() n = n + 1 return n endend
local c1, c2 = counter(), counter()print(c1(), c1(), c2()) -- => 1 2 1Two closures created in the same scope share the same upvalue — this is how you build a small object without a table:
local function account(balance) local function deposit(x) balance = balance + x end local function get() return balance end return deposit, getend
local deposit, get = account(100)deposit(50)print(get()) -- => 150Closures are also the standard way to keep a private cache or memo:
local function memoize(f) local cache = {} return function(x) local v = cache[x] if v == nil then v = f(x) cache[x] = v end return v endendTail calls
Section titled “Tail calls”return f(args) — with nothing else in the return statement — is a proper tail call. Lua reuses the current stack frame, so the recursion depth is unbounded.
local function loop(i, n) if i > n then return "done" end return loop(i + 1, n) -- proper tail callendprint(loop(1, 5000000)) -- => done, no stack overflowreturn f(x) + 1 and return (f(x)) are not tail calls; the extra operation and the parentheses both require the frame to survive. Tail calls also erase the frame from tracebacks, which shows up as (...tail calls...) in error output.
The colon syntax
Section titled “The colon syntax”obj:method(a, b) is exactly obj.method(obj, a, b). The receiver is passed as the first argument.
local s = "hello"print(s:sub(2, 3)) -- => elprint(string.sub(s, 2, 3)) -- => el (identical)In a definition, function T:m(a) implicitly declares a parameter named self:
local Stack = {}Stack.__index = Stack
function Stack.new() return setmetatable({ n = 0 }, Stack)end
function Stack:push(v) -- self is implicit self.n = self.n + 1 self[self.n] = v return self -- enables chainingend
function Stack:pop() if self.n == 0 then return nil end local v = self[self.n] self[self.n] = nil self.n = self.n - 1 return vend
local s = Stack.new()s:push(1):push(2)print(s:pop(), s:pop()) -- => 2 1Note the definition-vs-call symmetry is not enforced — function T.m(self, a) and function T:m(a) compile to the same thing, and either can be called with :.
Key points
Section titled “Key points”~=is inequality;..is concatenation;#is length. There is no!=,+=,++,continueor ternary.repeat ... untilcan see the body’s locals in its condition.- Numeric
forevaluates its bounds once, and the control variable is a fresh local each iteration. - Generic
foris driven by an iterator function, a state, and a control value. and/orreturn operands and short-circuit; thecond and a or bternary fails whenais falsy.- Multiple returns expand only in the last position of an expression list; parentheses truncate to one.
select("#", ...)is the only correct way to count varargs that may containnil.obj:m(x)isobj.m(obj, x), andfunction T:m(x)adds an implicitself.