Getting started
Lua tables for FiveM developers
Everything about Lua tables for FiveM scripts: arrays vs dictionaries, ipairs vs pairs, table.insert/remove/sort/concat, nested config tables, copying (references), sparse-array bugs and converting to JSON.
Overview
Tables are the only data structure Lua has, and FiveM scripts are made of them: configs, job lists, item definitions, player caches, NUI payloads. Most confusing bugs in beginner scripts — a loop that stops early, a config that changes by itself, a count that is wrong — are table bugs. This guide covers the patterns that come up constantly and the traps behind those bugs.
Arrays and dictionaries
-- An array: values with implicit keys 1, 2, 3
local jobs = { 'police', 'ambulance', 'mechanic' }
print(jobs[1]) -- police
print(#jobs) -- 3
-- A dictionary: explicit keys
local prices = { bread = 5, water = 2 }
print(prices.bread) -- 5
print(prices['water']) -- 2
-- Both, and nesting
local shop = {
label = '24/7',
coords = vector3(25.7, -1347.3, 29.5),
items = {
{ name = 'bread', price = 5 },
{ name = 'water', price = 2 },
},
}
print(shop.items[2].name) -- waterLooping: ipairs vs pairs
| `ipairs(t)` | `pairs(t)` | |
|---|---|---|
| Visits | Keys 1, 2, 3… until the first nil | Every key |
| Order | In order | Not guaranteed |
| Use for | Arrays | Dictionaries |
for i, job in ipairs(jobs) do
print(i, job)
end
for item, price in pairs(prices) do
print(item, price)
end
-- Plain numeric loop, often the fastest for arrays
for i = 1, #jobs do
print(jobs[i])
endThe table library
local list = { 'b', 'c' }
table.insert(list, 'd') -- append: b c d
table.insert(list, 1, 'a') -- insert at 1: a b c d
local removed = table.remove(list) -- remove last, returns 'd'
table.remove(list, 1) -- remove first
list[#list + 1] = 'e' -- append without a function call
table.sort(list) -- alphabetical
table.sort(shop.items, function(a, b) return a.price < b.price end)
print(table.concat(list, ', ')) -- join into a stringThe length operator and gaps
#t gives the length of the array part — but only when there are no nil holes. Setting an element in the middle to nil creates a gap, and #, ipairs and table.concat may stop at it or return confusing results.
local players = { 'A', 'B', 'C' }
players[2] = nil -- hole in the middle
print(#players) -- may be 3 or 1 — undefined
for _, p in ipairs(players) do print(p) end -- prints only A
-- Correct: remove, which shifts the rest down
local players2 = { 'A', 'B', 'C' }
table.remove(players2, 2) -- { 'A', 'C' }For collections keyed by player ID — which are naturally sparse — use a dictionary and count with a loop or a separate counter, never #.
Removing while looping
-- Wrong: forward loop skips the element after each removal
-- Right: walk from the end
for i = #vehicles, 1, -1 do
if not DoesEntityExist(vehicles[i]) then
table.remove(vehicles, i)
end
endTables are references
Assigning a table to another variable does not copy it; both names point to the same table. This is how a config “changes by itself”: a script modifies what it thinks is its own copy.
local a = { money = 100 }
local b = a
b.money = 0
print(a.money) -- 0 — same table
local function deepCopy(t)
if type(t) ~= 'table' then return t end
local out = {}
for k, v in pairs(t) do out[k] = deepCopy(v) end
return out
end
local c = deepCopy(a) -- an independent copyThe same applies across events: a table sent with an event arrives as a new copy on the other side, because it is serialised. Changing it there does not change the sender’s table.
Fast lookups instead of searches
-- Slow if called often: scans the array each time
local allowed = { 'police', 'sheriff', 'fib' }
-- Fast: one lookup
local ALLOWED = { police = true, sheriff = true, fib = true }
if ALLOWED[job] then
-- ...
endTurning “is X in this list?” into a key lookup is one of the simplest Lua performance wins.
Tables and JSON
local data = { name = 'Mike', items = { 'phone', 'id_card' } }
local text = json.encode(data) -- '{"items":["phone","id_card"],"name":"Mike"}'
local back = json.decode(text)
print(back.items[1]) -- phone- JSON is how tables are stored in a database text column, sent to NUI and exchanged with web APIs.
- Arrays with gaps and tables mixing numeric and string keys encode unpredictably — keep data clean.
json.decodereturns nil for invalid JSON; check before using the result.
Frequently asked questions
What is a table in Lua?
Lua’s only data structure. The same type serves as an array, a dictionary, an object or a mix of all three.
Why does #table return the wrong number?
The table has a nil gap, or it is a dictionary. # only counts the array part up to the first gap.
How do I copy a table in Lua?
Assignment copies only the reference. Write a small deep-copy function that builds a new table and copies each value, recursing into nested tables.
How do I add an item to a Lua array?
table.insert(list, value) or list[#list + 1] = value.
How do I convert a Lua table to JSON in FiveM?
json.encode(t) returns a JSON string and json.decode(s) turns it back into a table.
Need this built, not just explained?
Ten years of FiveM work, from Lua to NUI
Custom resources, React NUI, ESX / QBCore / Qbox integration, OneSync performance audits and security reviews — plus the websites and SEO around your server brand.
Related guides
- Getting startedThe Lua you need for FiveM, from zeroAlways declare variables with local. Lua has nil, booleans, numbers, strings, tables and functions; only nil and false are falsy (0 and empty strings are true). Use if/elseif/else, numeric for i = 1, 10, for k, v in pairs(t) and while. Concatenate strings with .. and format with string.format. CfxLua adds vector3, backtick hashes, CreateThread and Wait.
- EngineeringThe FiveM Lua errors you will see most, and their fixes“Attempt to index a nil value” means you used . or [] on something that is nil — the object you expected was never set. “Attempt to call a nil value” means the function does not exist. Arithmetic, concatenate and compare errors mean a value you used is nil. Syntax errors (“'end' expected”, “unexpected symbol”) point at a missing keyword or character near the named line.