Server callback returns nil: ESX, QBCore and ox_lib callbacks
ESX.TriggerServerCallback, QBCore TriggerCallback and lib.callback.await returning nil? How each callback works on the server and client, and the reasons they return nothing.
A client script asks the server for data, and what comes back is nil, or nothing at all:
SCRIPT ERROR: @my_script/client/main.lua:21: attempt to index a nil value (local 'result')Server callbacks are how the client asks the server a question and gets an answer. Each framework has its own, and ox_lib has one that works everywhere. This article shows the three and why they return nil.
ESX
Register on the server, trigger from the client:
-- server
ESX.RegisterServerCallback('my_script:getMoney', function(source, cb)
local xPlayer = ESX.GetPlayerFromId(source)
cb(xPlayer.getMoney())
end)-- client
ESX.TriggerServerCallback('my_script:getMoney', function(money)
print(money)
end)The server function gets source and cb, and you pass the answer to cb. Extra arguments from the client arrive after cb. The client function receives what cb sent.
QBCore
The names differ, the shape is the same:
-- server
local QBCore = exports['qb-core']:GetCoreObject()
QBCore.Functions.CreateCallback('my_script:getMoney', function(source, cb)
local Player = QBCore.Functions.GetPlayer(source)
cb(Player.PlayerData.money.cash)
end)-- client
local QBCore = exports['qb-core']:GetCoreObject()
QBCore.Functions.TriggerCallback('my_script:getMoney', function(money)
print(money)
end)If ESX or QBCore is nil inside the script, the callback never registers. See ESX is nil and QBCore is nil.
ox_lib
ox_lib callbacks do not need a framework, and the answer is a return value instead of a cb function:
-- server
lib.callback.register('my_script:getMoney', function(source)
local xPlayer = ESX.GetPlayerFromId(source)
return xPlayer.getMoney()
end)-- client
local money = lib.callback.await('my_script:getMoney', false)
print(money)The second argument of await is a delay in milliseconds between the call and the answer (use false for none). Arguments for the server follow it:
local ok = lib.callback.await('my_script:buy', false, 'burger', 2)lib.callback.register('my_script:buy', function(source, item, amount)
-- ...
return true
end)For the callback to exist, the script loads ox_lib in its manifest:
shared_script '@ox_lib/init.lua'Without it, lib is nil: see ox_lib init.lua not found.
The server can also call a client: lib.callback.await('name', playerId, ...) on the server, with lib.callback.register on the client. If you prefer no waiting, lib.callback('name', false, function(result) end) takes a function instead.
Why a callback returns nil
Almost every case is one of these:
- The name does not match. The register and trigger names must be identical, including case. A typo does not raise an error you will notice, it just never answers.
- It is not registered. The server file never ran: it is missing from
server_scripts, the resource did not start, or it failed before reaching the register line. - An error on the server, before the answer. If the server function errors, for example
xPlayeris nil, the answer is never sent. Check the server console, not the client one. - You never call
cbon every path. On ESX and QBCore, a branch that returns without callingcb(...)leaves the client waiting:
-- wrong: no cb when the player is missing
ESX.RegisterServerCallback('my_script:getJob', function(source, cb)
local xPlayer = ESX.GetPlayerFromId(source)
if xPlayer then
cb(xPlayer.job.name)
end
end)-- right
ESX.RegisterServerCallback('my_script:getJob', function(source, cb)
local xPlayer = ESX.GetPlayerFromId(source)
cb(xPlayer and xPlayer.job.name or nil)
end)- You forgot
returnon ox_lib. Alib.callback.registerfunction that has noreturnanswersnil. - You call too early. A client callback fired at script start can reach the server before its register ran, after a restart for example. Trigger it after the player has loaded.
- Mixed styles. A callback registered with
lib.callback.registercannot be called withESX.TriggerServerCallback. Use the matching pair.
Tip: to find out which case you have, add a
Guard against nil on the client
Treat the answer as something that can be missing:
local money = lib.callback.await('my_script:getMoney', false)
if not money then
return print('No answer from the server')
endFor the older frameworks, check the argument before indexing it:
QBCore.Functions.TriggerCallback('my_script:getJob', function(job)
if not job then return end
print(job)
end)Which one to use
- Writing for one framework only: its own callback is fine.
- Writing for several frameworks, or for QBox: use ox_lib, so one callback works everywhere.
- Writing new code:
lib.callbackreturns values and reads like a normal function, which makes mistakes easier to spot.
If you port a script between ESX and QBCore, the callback names are one row in the function map.
Checklist
| Symptom | Fix |
|---|---|
| Result is nil | Check the name matches exactly on both sides |
| Never answers | Make sure the server file is in the manifest and the resource started |
| Error in the server console | Fix it; a failing callback sends no answer |
| ESX or QBCore: no reply on some paths | Call cb(...) on every branch |
| ox_lib: nil result | Add return to the lib.callback.register function |
lib is nil |
Add shared_script '@ox_lib/init.lua' to the manifest |
Quick answers
What is the difference between a callback and an event?
An event sends a message and does not wait for an answer. A callback asks the other side and gets a value back, so the client can use data that only the server knows.
Can I use ox_lib callbacks on ESX or QBCore?
Yes. ox_lib is independent of the framework, so lib.callback works on ESX, QBCore and QBox as long as ox_lib is started and loaded by the script.
Why does lib.callback.await stop my script for a while?
It waits for the server's answer, so it yields the thread. If the server never answers because the callback is missing or errors, the thread keeps waiting or fails with an error.


