# Coding Practices for ESX (/docs/getting-started/coding-practices)

These are the conventions and techniques that keep ESX scripts readable, fast and hard to exploit. They apply to any FiveM Lua resource, with examples that use the ESX API. If you are new to ESX, build [your first resource](/docs/getting-started/first-resource) first and come back here.

## Style [#style]

### Naming [#naming]

| Kind | Convention | Example |
| --- | --- | --- |
| Local variables and functions | `camelCase` | `local playerCount`, `local function getClosestShop()` |
| Globals (avoid them) | `PascalCase` | `Config`, `MyResource` |
| Constants | `UPPER_SNAKE_CASE` | `local MAX_DISTANCE <const> = 3.0` |
| Events | `resource:action` | `my_shop:buyItem` |

Prefix every event and callback with your resource name. Two scripts that both register `buyItem` will fight each other; `my_shop:buyItem` will not.

### Formatting [#formatting]

Pick one indentation width (the ESX core uses 4 spaces) and stay consistent. Use a formatter such as [StyLua](https://github.com/JohnnyMorganz/StyLua) so you never argue about it again.

### Comments and annotations [#comments-and-annotations]

Comment *why* something is done, not *what* the next line does. For functions other people will call, add LuaLS annotations so editors can show types and catch mistakes. See [Annotations](/docs/getting-started/annotations).

## Locals over globals [#locals-over-globals]

```lua
MyCounter = 0          -- global: visible to every file in the resource
local myCounter = 0    -- local: visible only in this file or block
```

Use `local` for everything unless you deliberately share a value between files of the same resource. Locals are faster to access, cannot be overwritten by another file by accident and are freed when they go out of scope. Functions are values too, so `local function` is the default.

A local function must be defined **before** the code that calls it:

```lua
-- Wrong: startThread is nil at the time it is called
startThread()
local function startThread() end

-- Right
local function startThread() end
startThread()
```

## Don't repeat yourself [#dont-repeat-yourself]

If you copy the same five lines into three event handlers, move them into a function. In ESX scripts the usual candidates are "get the player and check the job" blocks:

```lua title="server/main.lua"
---@return table? xPlayer
local function getPoliceOfficer(source)
    local xPlayer = ESX.GetPlayerFromId(source)
    if xPlayer and xPlayer.job.name == 'police' then
        return xPlayer
    end
end

RegisterNetEvent('my_police:cuff', function(targetId)
    local officer = getPoliceOfficer(source)
    if not officer then return end
    -- ...
end)
```

## Loops and threads [#loops-and-threads]

### Always wait [#always-wait]

A `while` loop without `Wait` never yields. On the client the game freezes, on the server every resource stops. Every loop that repeats must call `Wait`.

### Wait as long as you can [#wait-as-long-as-you-can]

`Wait(0)` runs every frame. Only code that must react every frame needs it: drawing markers or text, or reading controls with `IsControlJustPressed`. Everything else can run much less often. A common pattern is a dynamic sleep that is long when the player is far away and short when close:

```lua title="client/main.lua"
local SHOP_COORDS <const> = vector3(25.7, -1347.3, 29.5)

CreateThread(function()
    while true do
        local sleep = 1000
        local distance = #(GetEntityCoords(PlayerPedId()) - SHOP_COORDS)

        if distance < 10.0 then
            sleep = 0
            DrawMarker(1, SHOP_COORDS.x, SHOP_COORDS.y, SHOP_COORDS.z - 1.0, 0, 0, 0, 0, 0, 0, 1.0, 1.0, 0.5, 50, 150, 255, 150, false, false, 2, false, nil, nil, false)

            if distance < 1.5 and IsControlJustPressed(0, 38) then -- E
                TriggerEvent('my_shop:open')
            end
        end

        Wait(sleep)
    end
end)
```

ESX also ships a helper for this pattern, documented under [Point](/docs/es_extended/client/imports/point).

### React to events instead of polling [#react-to-events-instead-of-polling]

The classic mistake is a thread that checks the job every frame forever:

```lua title="client/main.lua"
-- Wasteful: runs every frame for every player, even non-police
CreateThread(function()
    while true do
        Wait(0)
        if ESX.PlayerData.job and ESX.PlayerData.job.name == 'police' then
            if IsControlJustPressed(0, 38) then
                -- ...
            end
        end
    end
end)
```

Only run the loop while the player actually has the job, and restart it when the job changes:

```lua title="client/main.lua"
local policeThreadRunning = false

local function isPolice()
    return ESX.PlayerData.job and ESX.PlayerData.job.name == 'police'
end

local function startPoliceThread()
    if policeThreadRunning then return end
    policeThreadRunning = true

    CreateThread(function()
        while isPolice() do
            if IsControlJustPressed(0, 38) then
                -- ...
            end
            Wait(0)
        end
        policeThreadRunning = false
    end)
end

-- Job changed while playing
RegisterNetEvent('esx:setJob', function(job, lastJob)
    if job.name == 'police' then
        startPoliceThread()
    end
end)

-- Character loaded (or resource restarted while already in game)
RegisterNetEvent('esx:playerLoaded', function()
    if isPolice() then startPoliceThread() end
end)

if ESX.PlayerLoaded and isPolice() then
    startPoliceThread()
end
```

`esx:setJob` reaches the client with the new job and the previous job. The `imports.lua` file keeps `ESX.PlayerData.job` in sync, so the loop condition sees the change on its own.

For key presses, consider `RegisterKeyMapping` with a command instead of a loop at all. It costs nothing while idle and players can rebind the key.

## Cache expensive values [#cache-expensive-values]

Store values that do not change often instead of asking the game again and again.

```lua title="client/main.lua"
local playerId = PlayerId()                 -- never changes during a session
local serverId = GetPlayerServerId(playerId)
local ped = PlayerPedId()

-- The ped changes on respawn or model change; ESX tells you when
AddEventHandler('esx:playerPedChanged', function(newPed)
    ped = newPed
end)
```

With `@es_extended/imports.lua` loaded, `ESX.PlayerData.ped` is also kept current, and `ESX.PlayerData.coords` returns the live position of that ped.

Cache only what you use and clear entries you no longer need. A table that grows by one entry per player who ever joined is a memory leak.

## Use current natives and Lua idioms [#use-current-natives-and-lua-idioms]

| Outdated | Use instead | Why |
| --- | --- | --- |
| `GetPlayerPed(-1)` | `PlayerPedId()` | Direct, no lookup by player index |
| `GetDistanceBetweenCoords(x1, y1, z1, x2, y2, z2, true)` | `#(vec1 - vec2)` | Vector math runs in Lua without a native call |
| `GetHashKey('adder')` | `` `adder` `` or `joaat('adder')` | Backtick strings are hashed at compile time in CfxLua |
| `table.insert(t, v)` | `t[#t + 1] = v` | Avoids a function call in hot loops |
| `Citizen.CreateThread`, `Citizen.Wait` | `CreateThread`, `Wait` | Shorter aliases, same behavior |
| `TriggerEvent('esx:getSharedObject', cb)` | `@es_extended/imports.lua` | The event-based getter is legacy |

<Callout type="warn" title="Do not replace table.remove with nil">
Some guides suggest `t[i] = nil` instead of `table.remove(t, i)`. For arrays that is a bug: it leaves a hole, after which `#t` and `ipairs` stop at the wrong place. Use `table.remove` for arrays. Setting `nil` is only correct for keyed tables like `players[source] = nil`.
</Callout>

## Lua 5.4 [#lua-54]

Add `lua54 'yes'` to your `fxmanifest.lua`. All ESX core resources use it. Useful features:

- **`<const>`** marks a local as constant. Assigning to it is a compile error.
  ```lua
  local MAX_PLAYERS <const> = 64
  ```
- **`<close>`** runs the value's `__close` metamethod when it goes out of scope, useful for cleanup.
- **Integer division** `//` and native **bitwise operators** `&`, `|`, `~`, `<<`, `>>`.
- **`goto continue`** to skip to the next loop iteration.
- A faster VM and a generational garbage collector.

CfxLua also adds extras on top of Lua 5.4, such as compound assignment (`count += 1`) and backtick hashing. They work in FiveM but not in a plain Lua interpreter.

## Server security [#server-security]

Performance problems slow a server down; security problems empty its economy. The rules:

1. **Every net event is public.** Any client can trigger any `RegisterNetEvent` with any data. Validate types, ranges and permissions on the server.
2. **The server decides values.** Never accept prices, amounts of money or item names chosen by the client when the server can look them up.
3. **Check context.** Is the player near the shop (`xPlayer.getCoords(true)`)? Do they have the job? Is the target player close?
4. **Rate-limit expensive actions** and log suspicious ones.

```lua title="server/main.lua"
local SHOP_COORDS <const> = vector3(25.7, -1347.3, 29.5)
local PRICES <const> = { water = 25, bread = 15 }

RegisterNetEvent('my_shop:buy', function(itemName)
    local xPlayer = ESX.GetPlayerFromId(source)
    if not xPlayer then return end

    local price = PRICES[itemName]
    if not price then return end -- unknown item: ignore

    if #(xPlayer.getCoords(true) - SHOP_COORDS) > 5.0 then return end

    if xPlayer.canCarryItem(itemName, 1) and xPlayer.removeAccountMoney('money', price, 'Shop purchase') then
        xPlayer.addInventoryItem(itemName, 1)
    end
end)
```

## Related [#related]

- [Annotations](/docs/getting-started/annotations)
- [Performance guide](/docs/guides/performance)
- [Callbacks](/docs/guides/callbacks)
- [Client functions](/docs/es_extended/client/functions)
