Iterators: pairs, ipairs and generic for
Understand what the generic for really calls, write stateless and stateful iterators, and sort tables with comparators.
What generic for calls
local t = { 10, 20, 30, name = "ada" }
-- ipairs walks 1..n and stops at the first nil: array part only
for i, v in ipairs(t) do print(i, v) end --> 1 10, 2 20, 3 30
-- pairs walks every key, in an unspecified order: use it for maps
for k, v in pairs(t) do print(k, v) end
-- generic for is sugar for this loop, which explains the contract
local f, state, control = next, t, nil
while true do
local k, v = f(state, control)
if k == nil then break end
control = k
-- body
end
-- iterating a numeric range with a step
for i = 1, 10, 2 do io.write(i, " ") end --> 1 3 5 7 9
-- counting down
for i = 3, 1, -1 do io.write(i) end --> 321
-- Lua 5.4 adds a closing value, for iterators that hold a resource
-- for line in io.lines("file.txt") do ... end -- the file closes at the endpairsorder is unspecified and can change between runs if the table is rebuilt. Never depend on it for output.ipairsstops at the first nil hole. A table with{1, nil, 3}iterates once.- Modifying a table while iterating with
pairsis undefined. Collect the keys into a separate list first and iterate over that. - Use
table.sorton the key list when you need a deterministic order:for _, k in ipairs(sortedKeys) do .... - A numeric
forloop evaluates its bounds once, before the first iteration.
Writing your own iterator
-- a stateless iterator: the control variable carries all the state
local function range(n)
return function(_, i)
i = i + 1
if i <= n then return i, i * i end
end, nil, 0
end
for i, square in range(4) do print(i, square) end --> 1 1, 2 4, 3 9, 4 16
-- a stateful iterator: a closure over its own variables
local function words(text)
local pos = 0
return function()
pos = text:find("%S+", pos + 1)
if not pos then return nil end
local word = text:match("%S+", pos)
return word, pos
end
end
for word, at in words("the quick brown fox") do print(at, word) end
-- an iterator that returns a key and a value, so pairs-style usage works
local function entries(t)
local keys = {}
for k in pairs(t) do keys[#keys + 1] = k end
table.sort(keys, function(a, b) return tostring(a) < tostring(b) end)
local i = 0
return function()
i = i + 1
local k = keys[i]
if k == nil then return nil end
return k, t[k]
end
end
for k, v in entries({ b = 2, a = 1, c = 3 }) do print(k, v) end --> a 1, b 2, c 3
-- the __pairs metamethod lets a custom type control its own iteration
local Sorted = {}
Sorted.__index = Sorted
function Sorted.new(t) return setmetatable({ data = t }, Sorted) end
Sorted.__pairs = function(self) return entries(self.data) end
for k, v in pairs(Sorted.new({ z = 26, a = 1 })) do print(k, v) end| Iterator | State | Notes |
|---|---|---|
next, t, nil | The table itself | What pairs returns |
inext, t, 0 | The table itself | What ipairs returns |
| Closure over a cursor | The upvalue | Simple to write, allocates one closure |
| Function plus control value | The control value | Allocates nothing per step |
| Coroutine-based | The coroutine | For complex or recursive traversal |
The generic for takes three values: an iterator function, an invariant state, and an initial control value. Returning a closure is the readable approach; returning a function plus a control value is the allocation-free one, which matters in a per-frame game loop.
Sorting with comparators
local people = {
{ name = "ada", age = 36 },
{ name = "grace", age = 45 },
{ name = "alan", age = 41 },
}
-- table.sort is in-place and unstable
table.sort(people, function(a, b) return a.age < b.age end)
-- tie-break explicitly when stability would have mattered
table.sort(people, function(a, b)
if a.age ~= b.age then return a.age < b.age end
return a.name < b.name
end)
-- sorting keys of a map
local counts = { apple = 3, pear = 7, plum = 1 }
local keys = {}
for k in pairs(counts) do keys[#keys + 1] = k end
table.sort(keys, function(x, y)
if counts[x] ~= counts[y] then return counts[x] > counts[y] end
return x < y
end)
-- comparing strings: tostring is needed when the key type is mixed
table.sort(keys, function(a, b) return tostring(a) < tostring(b) end)
-- a comparator must be a strict weak ordering, or sort may throw
-- "invalid order function for sorting". Never use <= as the comparison.💡
Lua's
table.sort is not stable, so equal elements may come out in any order. If the original order matters, add a tie-breaker — often a captured index — rather than relying on the algorithm to preserve it.FAQ
Why does pairs give a different order each run?
The hash part of a table is ordered by the hash of each key and by the table's internal size. Nothing guarantees stability across runs, versions or even across the insertion of one key. Sort explicitly when order matters.
Can I delete from a table while iterating?
Setting an existing key to nil with
next is allowed. Adding new keys is not. The safe pattern is to collect the keys that should go, then remove them in a second pass.Related
Tables, metatables and object orientation Functions, closures and multiple returns
Last refreshed 2026-09-18.