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 end
  • pairs order is unspecified and can change between runs if the table is rebuilt. Never depend on it for output.
  • ipairs stops at the first nil hole. A table with {1, nil, 3} iterates once.
  • Modifying a table while iterating with pairs is undefined. Collect the keys into a separate list first and iterate over that.
  • Use table.sort on the key list when you need a deterministic order: for _, k in ipairs(sortedKeys) do ....
  • A numeric for loop 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
IteratorStateNotes
next, t, nilThe table itselfWhat pairs returns
inext, t, 0The table itselfWhat ipairs returns
Closure over a cursorThe upvalueSimple to write, allocates one closure
Function plus control valueThe control valueAllocates nothing per step
Coroutine-basedThe coroutineFor 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.

Tables, metatables and object orientation Functions, closures and multiple returns

Last refreshed 2026-09-18.