pairs vs ipairs in Luau: Which Loop Should You Use?
Quick answer: Use ipairs (or plain generalized iteration) when your table is an ordered list of values, because it walks indices 1, 2, 3… in order and stops at the first gap. Use pairs only when you need every key of a dictionary-style table, since it visits keys in an unspecified order.
If your table is a list of items where order matters — an inventory, a queue of enemies, a list of parts to move — use ipairs. If your table is a dictionary that maps arbitrary keys to values and order does not matter, use pairs. In modern Luau you can often skip both entirely: writing for key, value in myTable do uses generalized iteration and behaves like pairs, which is why understanding what each function actually guarantees still matters.
What does ipairs actually do?
ipairs iterates the array part of a table: consecutive integer keys starting at 1. It returns index-value pairs in strict numeric order and stops at the first missing entry.
local tools = { "Sword", "Bow", nil, "Wand" }
for i, tool in ipairs(tools) do
print(i, tool) -- prints 1 Sword, 2 Bow, then stops
end
The loop above never reaches "Wand" even though it sits at index 4. As soon as ipairs hits tools[3] == nil, iteration ends. This is not a bug — it is the documented contract, and it is exactly why ipairs is safe on arrays with trailing holes but wrong for sparse data. It also explains a classic mistake: storing nil in the middle of a list silently truncates any loop built on ipairs.
What does pairs do differently?
pairs visits every key stored in the table — strings, numbers, instances, whatever you used as a key — including integer keys beyond gaps. The one guarantee you give up is ordering:
local stats = {
Health = 100,
["Jump Power"] = 50,
WalkSpeed = 16,
}
for name, value in pairs(stats) do
print(name, value)
end
Run this twice in different sessions and the print order may differ. The Lua/Luau specification makes no promises about traversal order, so never write code whose correctness depends on it. If you need sorted output, collect the keys into an array and sort them yourself:
local keys = {}
for name in pairs(stats) do
table.insert(keys, name)
end
table.sort(keys)
for _, name in ipairs(keys) do
print(name, stats[name]) -- now deterministic: Health, Jump Power, WalkSpeed
end
This "collect keys, then sort" pattern is the standard answer whenever someone asks how to get alphabetical or priority-ordered iteration out of a dictionary.
What is generalized iteration in Luau?
Luau added generalized iteration: you can iterate a table directly in the for statement without naming a function at all.
local loot = { "Coin", "Coin", "Gem" }
-- Old style
for i, item in ipairs(loot) do
print(i, item)
end
-- Modern Luau
for i, item in loot do
print(i, item)
end
local stats = { Health = 100, Mana = 50 }
-- Modern Luau, dictionary style (like pairs)
for stat, value in stats do
print(stat, value)
end
For arrays, for i, v in t do behaves like ipairs; for mixed or dictionary tables it behaves like pairs. Under the hood the compiler picks the fastest safe strategy, and it skips the extra function-call overhead of pairs/ipairs, so the generalized form is both cleaner and typically faster. New Roblox codebases increasingly treat bare iteration as the default. The one caveat: because the array-like form stops at nil just like ipairs, all the same hole-in-the-array rules apply. If you want to see how tables back all of this, the guide on Luau tables, arrays, and dictionaries covers the underlying structure.
So should you ever write pairs or ipairs anymore? Yes — they are fully supported and clearer to readers coming from vanilla Lua, and ipairs communicates intent ("this is an ordered list") explicitly. But if you are starting fresh, prefer bare iteration.
When does order matter?
Ask one question about each table: if I shuffled the elements, would anything break?
- Order matters → array +
ipairs(or bare iteration). Examples: turn queues, path waypoints, dialogue lines, anything you render top-to-bottom. - Order irrelevant → dictionary +
pairs(or bare iteration). Examples: player settings keyed by name, cooldown timestamps keyed by ability, cached instances keyed by id.
A common hybrid is a dictionary whose values are arrays — e.g. per-player inventories. You iterate the outer table with pairs (player names, any order) and each inner inventory with ipairs (slots, fixed order):
local inventories = {
Alice = { "Sword", "Shield" },
Bob = { "Bow", "Arrow", "Arrow" },
}
for player, items in pairs(inventories) do
for slot, item in ipairs(items) do
print(player .. " slot " .. slot .. ": " .. item)
end
end
Can I remove items while iterating?
This is the pitfall that bites almost everyone once. Removing the current element during a pairs-style loop assigns nil to its key, which is allowed — but adding new keys during iteration is undefined behavior, and removing entries from an array mid-ipairs shifts every subsequent index, making you skip elements:
local enemies = { "Slime", "Bat", "Ghost", "Bat" }
-- WRONG: table.remove shifts indices, the loop skips "Ghost"
for i, enemy in ipairs(enemies) do
if enemy == "Bat" then
table.remove(enemies, i)
end
end
print(#enemies) -- 3, a Bat survived!
-- RIGHT: iterate backwards
for i = #enemies, 1, -1 do
if enemies[i] == "Bat" then
table.remove(enemies, i)
end
end
Iterating backwards works because removing index i only shifts elements after it — and you have already visited those. An alternative that avoids the issue entirely is building a second "keep" list and replacing the original afterwards. For delayed removals, pairing this pattern with predictable task.wait vs task.spawn timing keeps cleanup logic reliable inside loops that yield.
Which loop should you use? A quick matrix
| Situation | Use |
|---|---|
| Ordered list of values (array) | for i, v in t do or ipairs(t) |
| Key-value lookup (dictionary) | for k, v in t do or pairs(t) |
| Need sorted iteration | Collect keys, table.sort, then loop |
| Sparse data / holes in the middle | pairs-style iteration over explicit keys |
| Removing many array items | Backward numeric for-loop |
If you remember nothing else: ipairs means order guaranteed, stops at first nil; pairs means everything, order meaningless. Generalized iteration gives you whichever matches the shape of your table, so most of the decision reduces to designing your tables correctly in the first place.
Learn Luau by doing
Reading about loops only gets you so far — the nil-hole trap is something you want to hit in a sandbox, not in production code. On Luablox you can work through theory, solve targeted loop-and-table problems, and follow structured Luablox lessons that make these iteration rules muscle memory.
Frequently asked questions
Does ipairs stop at nil in Luau?
Yes. ipairs walks integer indices starting at 1 and terminates as soon as it encounters a nil value. If your array has a hole in the middle, everything after the hole is skipped, even if valid values exist at higher indices.
Is pairs slower than ipairs in Roblox?
Both are cheap, and the difference rarely matters in gameplay code. In practice, Luau generalized iteration (for k, v in t do) usually compiles to faster code than either, because the compiler can specialize the loop to the table type without the extra iterator-function calls.
What is generalized iteration in Luau?
Generalized iteration lets you write for k, v in someTable do without calling pairs or ipairs. For array-shaped tables it iterates like ipairs in order; otherwise it falls back to pairs-like whole-table traversal. It has been supported in Roblox since 2022 and is the recommended modern style.
How do I iterate a table in sorted order?
pairs gives no ordering guarantee, so collect the keys into an array with table.insert, call table.sort(keys) (optionally with a custom comparator), then loop over the sorted keys with ipairs and index back into the original table.
Why did my loop skip elements after using table.remove?
table.remove shifts every later element down by one index. If you remove while iterating forward, the element that moves into the current index is never visited. Iterate backwards from #t to 1 instead, or build a filtered copy of the table.
Understand loop structures in Luau, including numeric, while, generic pairs, and ipairs iterators.