Getting started
Lua tables for FiveM developers
Lua tables for FiveM scripts: arrays vs dictionaries, ipairs vs pairs, table functions, nested configs, copying by reference 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.
- EngineeringDesigning a config.lua people can actually usePut values owners change (locations, prices, jobs, feature toggles) in config.lua, keep logic out of it, and split server-only values (webhooks, tokens, reward amounts) into a server-side config that is never sent to clients. Group settings, name them clearly, ship working defaults, validate the config at startup with readable errors, and leave config files editable under escrow with escrow_ignore.
- EngineeringOptimising FiveM Lua scriptsSleep loops dynamically (long Wait when nothing is nearby, Wait(0) only when you must draw or read input), replace polling with events and state bags, measure distance with #(a - b) on vectors, read values like the player ped once per tick (or from ox_lib’s cache), use backtick hashes, avoid creating tables and strings in hot loops, and batch database writes. Measure before and after with resmon and the profiler.
- 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.