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
ensureto restart your resource andrestartto restart a single resource. Checkresmonto 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
pcallto 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.