Guide 2026-05-03

FiveM Debugging Techniques & Common Errors

TDYSKY

TDYSKY

Founder & Lead Developer at Agency Scripts

The Art of Debugging FiveM Scripts

Debugging FiveM scripts is fundamentally different from debugging standard applications. You are dealing with a split client-server architecture where code runs on two separate runtimes, events travel across the network, and game state changes every frame. When something goes wrong, the error might originate on the server but manifest on the client, or vice versa. Developing a systematic approach to debugging will save you countless hours and make your development workflow dramatically faster. This guide covers the essential tools, techniques, and patterns that professional FiveM developers use daily.

Using Print Statements Effectively

The most fundamental debugging tool in FiveM is the print() function, but using it effectively requires more than just dumping variables. Structure your debug output with prefixes that identify the script, the side (client or server), and the function where the print occurs. Use color codes to make important messages stand out in the console. Build a debug utility that you can toggle on and off without removing debug lines from your code.

-- shared/debug.lua
local DEBUG_ENABLED = GetConvar('myresource_debug', 'false') == 'true'
local RESOURCE_NAME = GetCurrentResourceName()

function DebugLog(module, message, ...)
    if not DEBUG_ENABLED then return end

    local side = IsDuplicityVersion() and 'SERVER' else 'CLIENT'
    local formatted = type(message) == 'string' and message:format(...) or tostring(message)
    local timestamp = os.date('%H:%M:%S')

    print(('[^3%s^0][^5%s^0][^2%s^0] %s'):format(
        timestamp, RESOURCE_NAME, side .. ':' .. module, formatted
    ))
end

function DebugTable(module, tbl, depth)
    if not DEBUG_ENABLED then return end
    depth = depth or 0
    local indent = string.rep('  ', depth)

    if type(tbl) ~= 'table' then
        DebugLog(module, '%s%s', indent, tostring(tbl))
        return
    end

    for k, v in pairs(tbl) do
        if type(v) == 'table' then
            DebugLog(module, '%s%s = {', indent, tostring(k))
            DebugTable(module, v, depth + 1)
            DebugLog(module, '%s}', indent)
        else
            DebugLog(module, '%s%s = %s (%s)', indent, tostring(k), tostring(v), type(v))
        end
    end
end

Using the Debug Utility

With this utility in place, you can add debug logging throughout your scripts that is completely silent in production. Enable it by adding set myresource_debug true to your server config when you need to troubleshoot. The structured output makes it easy to grep through console logs and find exactly what you are looking for.

-- server/jobs.lua
RegisterNetEvent('myresource:startJob', function(jobName)
    local src = source
    DebugLog('jobs', 'Player %d attempting to start job: %s', src, jobName)

    local playerData = GetPlayerData(src)
    DebugTable('jobs', playerData)

    if not playerData then
        DebugLog('jobs', 'ERROR: No player data found for source %d', src)
        return
    end

    if playerData.job == jobName then
        DebugLog('jobs', 'Player %d already has job %s, skipping', src, jobName)
        return
    end

    DebugLog('jobs', 'Job %s assigned to player %d successfully', jobName, src)
end)

Common FiveM Errors and Solutions

SCRIPT ERROR: attempt to index a nil value

This is the most frequent Lua error in FiveM development. It means you are trying to access a property or method on a variable that is nil. The most common causes are accessing player data before it is loaded, referencing a framework function that does not exist in the version you are using, or reading a config value with a typo in the key. Always check for nil before accessing nested properties.

-- BAD: Will crash if GetPlayerData returns nil
local name = GetPlayerData(src).charinfo.firstname

-- GOOD: Defensive nil checks
local playerData = GetPlayerData(src)
if not playerData then
    print('[ERROR] Player data is nil for source: ' .. src)
    return
end

local charinfo = playerData.charinfo
if not charinfo then
    print('[ERROR] charinfo missing for source: ' .. src)
    return
end

local name = charinfo.firstname or 'Unknown'

SCRIPT ERROR: attempt to call a nil value

This error occurs when you try to call a function that does not exist. In FiveM, this commonly happens when you call an export from a resource that has not started yet, use a framework function that was renamed in an update, or forget to load a shared file in your manifest. Check your fxmanifest.lua to make sure all required files are listed and in the correct order.

-- Safely calling an export that might not be available
local function SafeExport(resource, exportName, ...)
    local success, result = pcall(function(...)
        return exports[resource][exportName](...)
    end, ...)

    if not success then
        print(('[^1ERROR^0] Failed to call export %s:%s - %s'):format(
            resource, exportName, tostring(result)
        ))
        return nil
    end

    return result
end

-- Usage
local inventory = SafeExport('ox_inventory', 'GetInventory', src)

Event was not registered

When you trigger an event that no script has registered a handler for, FiveM silently drops it with no error in the console. This can be maddening to debug because everything looks correct but nothing happens. Use a helper function to verify events are registered before triggering them, and add logging around your event triggers during development.

-- server/debug_events.lua
-- Wrap TriggerClientEvent to log when events fire
local originalTrigger = TriggerClientEvent

if GetConvar('myresource_debug', 'false') == 'true' then
    TriggerClientEvent = function(eventName, target, ...)
        print(('[^3EVENT^0] TriggerClientEvent: %s -> target: %s'):format(
            eventName, tostring(target)
        ))
        return originalTrigger(eventName, target, ...)
    end
end

NUI Debugging with DevTools

For scripts with NUI interfaces, the built-in Chromium DevTools are invaluable. Open them with the F8 console by typing nui_devtools to access the full Chrome inspector. This gives you the Elements panel to inspect DOM structure, the Console for JavaScript errors, the Network tab for resource loading, and the Sources panel for setting breakpoints. For NUI communication issues, log both sides of the message bridge.

// nui/js/debug.js
// Log all incoming NUI messages
window.addEventListener('message', (event) => {
    if (event.data && event.data.action) {
        console.log(
            '%c[NUI Received]%c ' + event.data.action,
            'background: #2dd4bf; color: #000; padding: 2px 6px; border-radius: 3px;',
            'color: #94a3b8;',
            event.data
        );
    }
});

// Wrap fetch to log NUI callbacks
const originalFetch = window.fetch;
window.fetch = function(url, options) {
    const body = options?.body ? JSON.parse(options.body) : null;
    console.log(
        '%c[NUI Callback]%c ' + url,
        'background: #8b5cf6; color: #fff; padding: 2px 6px; border-radius: 3px;',
        'color: #94a3b8;',
        body
    );
    return originalFetch.apply(this, arguments);
};

Profiling with Resmon and Timing

Beyond basic resmon monitoring, you can build precise timing instrumentation into your scripts. Measure how long specific operations take and log warnings when they exceed acceptable thresholds. This is especially important for database queries, complex calculations, and loops that process many entities.

-- shared/profiler.lua
local Profiler = {}

function Profiler.Start(label)
    return {
        label = label,
        startTime = GetGameTimer()
    }
end

function Profiler.Stop(timer, warnThresholdMs)
    local elapsed = GetGameTimer() - timer.startTime
    warnThresholdMs = warnThresholdMs or 5

    if elapsed >= warnThresholdMs then
        print(('[^1PERF WARNING^0] %s took %dms (threshold: %dms)'):format(
            timer.label, elapsed, warnThresholdMs
        ))
    elseif GetConvar('myresource_debug', 'false') == 'true' then
        print(('[^2PERF^0] %s completed in %dms'):format(timer.label, elapsed))
    end

    return elapsed
end

-- Usage in a server event
RegisterNetEvent('myresource:heavyOperation', function(data)
    local timer = Profiler.Start('heavyOperation')

    -- ... expensive processing ...
    local result = ProcessLargeDataSet(data)

    Profiler.Stop(timer, 10) -- warn if over 10ms
end)

State Bag Debugging

State bags are a powerful but sometimes confusing feature. When state bag values do not update as expected, it is usually because you are setting them on the wrong entity, the handler is not catching the correct bag name, or replication is delayed. Build a state bag inspector command that dumps all state for a given entity.

-- server/debug_statebags.lua
RegisterCommand('debugstate', function(source, args)
    local targetId = tonumber(args[1])
    if not targetId then
        print('Usage: debugstate [playerId]')
        return
    end

    local playerPed = GetPlayerPed(targetId)
    if playerPed == 0 then
        print('Player not found: ' .. targetId)
        return
    end

    local entityState = Player(targetId).state
    print(('[^3STATE BAGS^0] Player %d:'):format(targetId))

    -- Print known state keys (state bags don't have an iterator)
    local keysToCheck = {'job', 'gang', 'duty', 'dead', 'phone', 'inventory'}
    for _, key in ipairs(keysToCheck) do
        local val = entityState[key]
        if val ~= nil then
            print(('  %s = %s (%s)'):format(key, tostring(val), type(val)))
        end
    end
end, true)

Essential Debugging Checklist

  • Check both consoles. Always look at both the server console (txAdmin or terminal) and the client console (F8) for errors. An error on one side often explains broken behavior on the other.
  • Verify resource state. Use ensure to restart your resource and restart to restart a single resource. Check resmon to make sure the resource is actually running.
  • Test with a clean environment. Disable other scripts that interact with the same systems. Many bugs come from conflicts between resources rather than bugs within a single script.
  • Read the error stack trace carefully. Lua stack traces show you the exact file and line number. Read them from bottom to top to understand the call chain that led to the error.
  • Use pcall for risky operations. Wrap database queries, export calls, and JSON decoding in pcall to catch errors gracefully instead of letting them crash your script.
  • Version your configs. When players report bugs, ask which version they are running. Many issues stem from outdated configuration files after an update.

Share this article

Ready to upgrade your server?

Check out our premium FiveM scripts in the Agency Scripts store or join our Discord community for support and updates.