DEV Community

xFiveM Shop
xFiveM Shop

Posted on

Validate Your FiveM Script's config.lua at Startup (and Fail Loudly)

Most FiveM job scripts ship with a config.lua that server owners are expected to edit. That is a good thing: it means you can change payouts, locations and job names without touching the logic. It also means the config is the part of the resource most likely to break. A missing comma, a job name with a capital letter, a vector3 with only two numbers, or a minimum payout bigger than the maximum, and the script either throws an error deep inside a thread or, worse, keeps running with nonsense values.

This post shows a small pattern for checking the config when the resource starts, printing clear messages, and refusing to run the job logic until the config is fixed. It needs no extra libraries, so you can drop it into any resource.

An example config

Here is a typical config for a simple delivery job:

-- shared/config.lua
Config = {}

Config.Job = 'delivery'          -- must match a job name in your framework
Config.RequiredItem = 'package'  -- item the player must carry
Config.Payout = { min = 150, max = 300 }
Config.Cooldown = 600            -- seconds between runs

Config.Depots = {
    { label = 'Docks',   coords = vector3(1205.4, -3110.2, 5.5), heading = 90.0 },
    { label = 'Airport', coords = vector3(-1037.8, -2737.6, 20.2), heading = 330.0 },
}
Enter fullscreen mode Exit fullscreen mode

Everything here can go wrong after a server owner edits it. The goal is to catch those mistakes on the first start rather than when the first player clocks in.

The idea

The pattern has three parts:

  1. A few tiny helper functions that record errors instead of throwing them.
  2. A validateConfig function that checks each field and collects every problem it finds.
  3. A flag, ConfigOk, that the rest of the server code checks before doing anything.

Collecting all errors at once matters: if the validator stops at the first problem, the owner fixes one line, restarts, and hits the next one.

The helpers

-- server/validate.lua
local errors = {}

local function fail(path, msg)
    errors[#errors + 1] = ('%s: %s'):format(path, msg)
end

local function expectType(path, value, expected)
    local actual = type(value)
    if actual ~= expected then
        fail(path, ('expected %s, got %s'):format(expected, actual))
        return false
    end
    return true
end

local function expectNumberRange(path, value, min, max)
    if not expectType(path, value, 'number') then return false end
    if (min and value < min) or (max and value > max) then
        fail(path, ('%s is outside the allowed range %s to %s')
            :format(value, tostring(min), tostring(max)))
        return false
    end
    return true
end
Enter fullscreen mode Exit fullscreen mode

One detail worth knowing: in FiveM's Lua runtime, vectors are their own type. type(vector3(1.0, 2.0, 3.0)) returns 'vector3', not 'table' or 'userdata'. That makes it easy to check that coordinates really are vectors, which catches the common mistake of writing a plain table like {1205.4, -3110.2, 5.5} or using vector2 by accident.

The validator

local function validateConfig()
    if not expectType('Config', Config, 'table') then return end

    if expectType('Config.Job', Config.Job, 'string') and Config.Job == '' then
        fail('Config.Job', 'must not be empty')
    end

    expectType('Config.RequiredItem', Config.RequiredItem, 'string')

    if expectType('Config.Payout', Config.Payout, 'table') then
        local okMin = expectNumberRange('Config.Payout.min', Config.Payout.min, 0)
        local okMax = expectNumberRange('Config.Payout.max', Config.Payout.max, 0)
        if okMin and okMax and Config.Payout.min > Config.Payout.max then
            fail('Config.Payout', 'min is larger than max')
        end
    end

    expectNumberRange('Config.Cooldown', Config.Cooldown, 0, 86400)

    if expectType('Config.Depots', Config.Depots, 'table') then
        if #Config.Depots == 0 then
            fail('Config.Depots', 'needs at least one depot')
        end
        local seen = {}
        for i, depot in ipairs(Config.Depots) do
            local p = ('Config.Depots[%d]'):format(i)
            if expectType(p, depot, 'table') then
                if expectType(p .. '.label', depot.label, 'string') then
                    if seen[depot.label] then
                        fail(p .. '.label', ('duplicate label "%s"'):format(depot.label))
                    end
                    seen[depot.label] = true
                end
                expectType(p .. '.coords', depot.coords, 'vector3')
                expectNumberRange(p .. '.heading', depot.heading, 0, 360)
            end
        end
    end
end
Enter fullscreen mode Exit fullscreen mode

Each check uses a readable path such as Config.Depots[2].coords, so the message points the owner straight at the line to fix.

Running it on start

ConfigOk = false

CreateThread(function()
    validateConfig()

    local resource = GetCurrentResourceName()
    if #errors > 0 then
        print(('^1[%s] %d config problem(s) found:^7'):format(resource, #errors))
        for _, e in ipairs(errors) do
            print(('^1[%s]^7   %s'):format(resource, e))
        end
        print(('^1[%s] job is disabled until config.lua is fixed.^7'):format(resource))
        return
    end

    ConfigOk = true
    print(('^2[%s] config OK^7'):format(resource))
end)
Enter fullscreen mode Exit fullscreen mode

The ^1, ^2 and ^7 codes colour the console output red, green and back to white, which makes problems hard to miss in a busy server log.

Then guard your event handlers and callbacks:

RegisterNetEvent('delivery:startRun', function(depotIndex)
    if not ConfigOk then return end
    local src = source
    local depot = Config.Depots[tonumber(depotIndex) or 0]
    if not depot then return end
    -- normal job logic continues here
end)
Enter fullscreen mode Exit fullscreen mode

The resource stays started, so nothing else on the server breaks, but the job simply refuses to run until the config is valid. That is far better than paying out nil or spawning a vehicle at the bottom of the ocean.

Checking against the framework

Type checks catch syntax-level mistakes. The most common real-world problem, though, is a job or item name that does not exist on this particular server. You can add an optional second pass that asks the framework.

For ESX Legacy, the server object exposes the loaded jobs and item labels:

local function validateAgainstESX(ESX)
    local jobs = ESX.GetJobs()
    if jobs and not jobs[Config.Job] then
        fail('Config.Job', ('job "%s" is not registered in ESX'):format(Config.Job))
    end
    if not ESX.GetItemLabel(Config.RequiredItem) then
        fail('Config.RequiredItem', ('item "%s" does not exist'):format(Config.RequiredItem))
    end
end
Enter fullscreen mode Exit fullscreen mode

For QBCore, the shared tables serve the same purpose: QBCore.Shared.Jobs[Config.Job] and QBCore.Shared.Items[Config.RequiredItem]. If your server uses a separate inventory resource, check items through that inventory's own API instead, because the framework's item list may be empty.

Two cautions. First, run this pass a moment after start, once the framework has finished loading its jobs and items; checking too early can report false errors. Second, exact function names can differ between framework versions, so confirm them against the version you run. Wrapping the framework pass in pcall keeps a version mismatch from crashing your validator.

Small habits that make configs safer

  • Comment every field with its unit and an example, as in the config above. "Seconds" versus "minutes" causes more bugs than bad code.
  • Keep secrets out of shared files. Anything in a shared_script is sent to every client, so webhook URLs and API keys belong in a server-only file.
  • Ship defaults. Fill missing optional values with sensible defaults instead of failing, and reserve hard errors for values the job cannot run without.

The open source advantage

When you can read and edit a resource's code, you can add a validator like this yourself in a few minutes, even if the original author did not. That is one of the practical benefits of open source releases such as the ESX job scripts on xFiveM Shop: you are not stuck guessing why a locked file fails silently. If you are setting up a new resource and want the general install steps first, see xfivem.shop/help/installation.

Summary

A config validator is around a hundred lines of plain Lua. It turns vague runtime errors into a clear list of problems on the first start and protects your economy from broken values. Add it once, then reuse it across resources.

Top comments (0)