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:

text
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:

lua
-- server
ESX.RegisterServerCallback('my_script:getMoney', function(source, cb)
    local xPlayer = ESX.GetPlayerFromId(source)
    cb(xPlayer.getMoney())
end)
lua
-- 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:

lua
-- 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)
lua
-- 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:

lua
-- server
lib.callback.register('my_script:getMoney', function(source)
    local xPlayer = ESX.GetPlayerFromId(source)
    return xPlayer.getMoney()
end)
lua
-- 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:

lua
local ok = lib.callback.await('my_script:buy', false, 'burger', 2)
lua
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:

lua
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:

  1. 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.
  2. 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.
  3. An error on the server, before the answer. If the server function errors, for example xPlayer is nil, the answer is never sent. Check the server console, not the client one.
  4. You never call cb on every path. On ESX and QBCore, a branch that returns without calling cb(...) leaves the client waiting:
lua
-- 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)
lua
-- right
ESX.RegisterServerCallback('my_script:getJob', function(source, cb)
    local xPlayer = ESX.GetPlayerFromId(source)
    cb(xPlayer and xPlayer.job.name or nil)
end)
  1. You forgot return on ox_lib. A lib.callback.register function that has no return answers nil.
  2. 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.
  3. Mixed styles. A callback registered with lib.callback.register cannot be called with ESX.TriggerServerCallback. Use the matching pair.

Tip: to find out which case you have, add a print as the first line of the server function. No print means steps 1 or 2, a print without an answer means 3 to 5.

Guard against nil on the client

Treat the answer as something that can be missing:

lua
local money = lib.callback.await('my_script:getMoney', false)
if not money then
    return print('No answer from the server')
end

For the older frameworks, check the argument before indexing it:

lua
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.callback returns 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.

Scripts that skip this problem

Advanced BoostingTablet-driven vehicle boosting: contracts from class D to S+, crews and a live queue.View script →Quest CreatorA visual editor for quests and NPC dialogues, built node by node in game.View script →Pawn Shop AppA player-to-player pawn market inside lb-phone.View script →

Keep reading