Event

util.event.emit_callback

Shared

Fires an event and returns a promise that resolves with the handler's return values, or rejects if the handler fails

Syntax

local result = util.event.emit_callback(
    name,
    options = nil,
    ...
)

options is positional — pass nil or omit it entirely when no routing is needed.

  • When routing options are needed — pass the options table as the second argument.
  • When no routing options are needed — omit options and pass arguments directly after name.

Handlers are invoked in priority order, highest first.

  • Different priority — the handler with the higher priority fires first.
  • Same priority — handlers fire in registration order.

Only serializable values survive remote transmission. Non-serializable values are silently dropped.

  • Safe to send — nil, bool, number, string, table (including nested tables).
  • Silently dropped — functions, userdata, threads, and any other non-serializable Lua values.
  • Local-only emit (remote = false) — no serialization occurs; all Lua values are forwarded as-is.
  • Nested tables — fully supported, but deeply nested structures increase payload size and serialization cost.

On the server, a client's remote event only reaches handlers registered with remote = true.

  • Handlers without it are local-only and are skipped, so a client cannot trigger them.
  • Handlers that do receive a client's event see variable client set to the sender's peer id.
  • Refer to the Event Cheatsheet.

Parameters

TypeNameDescription
stringnameName of the event to fire
tableoptionsRouting options table
Refer Options section
any...Arguments forwarded to every handler

Returns

Always returns a single promise regardless of how many handlers are registered or whether the call is local or remote.
For remote calls, the remote side aggregates all of its handlers using the same rules before sending a single reply — so the caller's promise resolves with the same structure described below.

How that promise resolves depends on the number of registered handlers and the collect_all option:

collect_allHandlersResolved value
falseNoneResolves immediately with no values
falseOne or moreResolves directly with the first handler's return value(s), unpacked
"First" means highest priority, then registration order
trueNoneResolves immediately with no values
trueOne or moreResolves with {[1]={...}, [2]={...}, ...}
Each slot is always a table of that handler's return values, even if only one value was returned

Failure Cases

A handler that throws a Lua error never leaves the promise pending. The error message is printed to the console of the side where the handler ran. It is not passed to the caller.

SituationResult
collect_all = false, the handler errorsPromise is rejected - await returns ok = false with no values
Only the first handler is fired, the remaining handlers are not tried
collect_all = true, a handler errorsPromise still resolves - that handler's slot is an empty table {}, all other slots hold their return values
This also applies when every handler errors
Remote call, the remote handler errorsSame rules as above - a single handler rejects the caller's promise, with collect_all the slot is empty
Server calls a peer that is not connectedPromise is rejected immediately and a warning is logged
A client calls the server and no handler there has remote = truePromise resolves with no values
With collect_all = true, only handlers registered with remote = true are called and counted
Remote side never repliesPromise is rejected after 30 seconds (e.g. the peer disconnected or the resource stopped)

An empty slot cannot be told apart from a handler that returned nothing.

With collect_all = true, results[i][1] == nil means either the handler failed or it simply returned no value. Return an explicit value (e.g. true) from handlers whose success you need to check.


Options

Server to client util.event.emit_callback requires a valid peer.

  • peer is mandatory when calling with { remote = true } from the server — omitting it throws an error.
  • peer should refer to a currently connected peer — a disconnected or invalid peer-id does not throw: a warning is logged and the returned promise is rejected immediately (ok = false).
  • Broadcasting a callback to multiple clients is not supported — use util.event.emit instead if you need to notify all peers without expecting a reply.
TypeKeyDefaultSideDescription
boolremotefalseShared• When true - transmits the event to the remote side; client sends to the server, server sends to the peer given by peer
• When false - fires handlers on the current side only
intpeer0ServerTarget peer-id for server-side remote dispatch (ignored on client)
• Required on the server when remote = true - omitting it or passing 0 throws an error
• Sends to that specific peer only
boolcollect_allfalseShared• When false - only the highest-priority handler is fired and its return values are resolved directly, unpacked
• When true - all registered handlers are fired, in priority order, and results are resolved as a table of tables, one entry per handler

Examples

Await a return value from the first local handler
util.event.on("on_get_score", function()
    return 42
end)

util.thread.create(function(self)
    local ok, score = self:await(util.event.emit_callback("on_get_score"))
    core.engine.print("info", "Score:", score) -- 42
end):resume()
Collect return values from all local handlers using collect_all
util.event.on("on_query", function() return "handler_a" end)
util.event.on("on_query", function() return "handler_b" end)

util.thread.create(function(self)
    local ok, results = self:await(util.event.emit_callback("on_query", { collect_all = true }))
    core.engine.print("info", "Handler 1 returned:", results[1][1]) -- 'handler_a'
    core.engine.print("info", "Handler 2 returned:", results[2][1]) -- 'handler_b'
end):resume()
Multiple handlers each returning multiple values and being collected using collect_all
util.event.on("on_status", function() return "ok", 100 end)
util.event.on("on_status", function() return "ok", 200 end)

util.thread.create(function(self)
    local ok, results = self:await(util.event.emit_callback("on_status", { collect_all = true }))
    core.engine.print("info", results[1][1], results[1][2]) -- 'ok'  100
    core.engine.print("info", results[2][1], results[2][2]) -- 'ok'  200
end):resume()
Send a callback from client to server and await the reply
if core.engine.get_platform() == "client" then
    util.thread.create(function(self)
        local ok, result, data = self:await(util.event.emit_callback("on_fetch_inventory", { remote = true }, "weapons"))
        core.engine.print("info", "Server replied:", result) -- 'ok'
        core.engine.iprint(data)
    end):resume()
else
    util.event.on("on_fetch_inventory", function(category)
        local thread = util.thread.current()
        thread:sleep(2000)
        return "ok", { category = category, items = { "sword", "bow" } }
    end, { async = true })
end
Send a callback from server to a specific client and await the reply
if core.engine.get_platform() == "client" then
    util.event.on("on_request_position", function()
        local thread = util.thread.current()
        thread:sleep(500)
        return 12.5, 0.0, 87.3
    end, { async = true })
else
    util.thread.create(function(self)
        local ok, x, y, z = self:await(util.event.emit_callback("on_request_position", { remote = true, peer = 1963414407 }))
        core.engine.print("info", "Client position:", x, y, z)
    end):resume()
end
Collect aggregated results from multiple handlers on the remote client
if core.engine.get_platform() == "client" then
    util.event.on("on_status", function() return "ok" end)
    util.event.on("on_status", function() return "ok", { detail = "extra" } end)
else
    util.thread.create(function(self)
        local ok, results = self:await(util.event.emit_callback("on_status", { remote = true, peer = 1963414407, collect_all = true }))
        core.engine.print("info", "Handler 1:", results[1][1]) -- 'ok'
        core.engine.print("info", "Handler 2:", results[2][1], tostring(results[2][2])) -- 'ok'  'table: 0x...'
    end):resume()
end
Handle a handler error
util.event.on("on_risky", function() error("boom") end)
util.event.on("on_risky", function() return "fine" end)

util.thread.create(function(self)
    -- Without collect_all only the first handler runs - it errors, so the promise is rejected
    local ok, value = self:await(util.event.emit_callback("on_risky"))
    core.engine.print("info", ok, value) -- false  nil

    -- With collect_all the failing handler is an empty slot, the others still return
    local ok, results = self:await(util.event.emit_callback("on_risky", { collect_all = true }))
    core.engine.print("info", ok, #results[1], results[2][1]) -- true  0  'fine'
end):resume()
Skip a peer that is not connected
util.thread.create(function(self)
    local ok, result = self:await(util.event.emit_callback("on_request_position", { remote = true, peer = 123456 }))
    if not ok then
        core.engine.print("warn", "Request failed (peer not connected or handler error)")
        return
    end
    core.engine.print("info", "Reply:", result)
end):resume()

On this page