JavaScript event and export contracts
The official JavaScript reference sources document separate client and server forms. emitNet has a different argument list on each side. This is the source of many handlers that appear to receive a shifted or missing payload.
Choose the side before the function
| Function | Client script | Server script |
|---|---|---|
on(name, callback) | Receive a local client event. | Receive a local server event. |
emit(name, ...args) | Trigger within this client’s runtime context. | Trigger within the server context. |
onNet(name, callback) | Receive network or local invocation. | Receive network or local invocation. |
emitNet(name, ...args) | Send to the server. There is no target-player parameter. | Not this overload: a target is required. |
emitNet(name, target, ...args) | Not the client overload; the supposed target becomes payload. | Send to one server session ID; -1 explicitly broadcasts. |
exports(name, callback) | Expose a function to client resources. | Expose a function to server resources. |
onNet is not server-only authentication. Do not expose internal server mutations merely to share a function with another server resource. An export is a same-side resource interface, not a network endpoint; the receiver still needs to protect any privileged operation. For tick scheduling and player snapshots, use setTick, clearTick and getPlayers.
A bounded diagnostic exchange
Create a new resource with this manifest. This example has no database or framework dependency and returns only the requesting player’s short diagnostic text.
fx_version 'cerulean'
game 'gta5'
client_script 'client.js'
server_script 'server.js'Create server.js:
const lastRequest = new Map();
onNet('doc:echoRequest', function (text) {
const sender = Number(global.source);
if (!Number.isSafeInteger(sender) || sender <= 0 || !GetPlayerName(sender)) return;
if (typeof text !== 'string' || text.length === 0 || text.length > 80) return;
const now = GetGameTimer();
const previous = lastRequest.get(sender);
if (previous !== undefined && now >= previous && now - previous < 1000) return;
lastRequest.set(sender, now);
emitNet('doc:echoReply', sender, { text });
});
on('playerDropped', function () {
lastRequest.delete(Number(global.source));
});
exports('getProtocolVersion', () => 1);Create client.js:
RegisterCommand('docecho', (_source, args) => {
const text = args.join(' ');
if (text.length > 0 && text.length <= 80) emitNet('doc:echoRequest', text);
}, false);
onNet('doc:echoReply', (message) => {
if (!message || typeof message.text !== 'string' || message.text.length > 80) return;
console.log(`Server echo: ${message.text}`);
});Start the resource and enter docecho connection probe in the client’s F8 console. Expect one reply in that client’s console. Repeat within one second: the server suppresses the additional request. A second player must not receive the first player’s reply. Empty, overlong and non-string input should produce no reply. Stopping the resource removes its runtime; it does not preserve the rate-limit map.
The client output is a diagnostic, not a trusted acknowledgement for money, permissions or inventory. A modified client can fabricate its own output. A production mutation needs server-owned state, operation-specific authorization and idempotency. See secure requests.
Source lifetime and asynchronous work
Capture global.source before an await, timer or I/O callback. Do not write const source = source: that accesses the new lexical binding before initialization. Preserve the initiating session identity as well as its number, because a disconnected player’s ID can be reused before the callback completes.
The example above performs no asynchronous work between capture and reply. For actual Node I/O, async interoperability covers scheduler handoff and stale-session checks. A timeout on an application reply does not cancel the underlying database or HTTP operation.
Export consumers and teardown
The exported function above is called from another resource on the same side through exports['providerResource'].getProtocolVersion(), where providerResource is the actual folder/resource name. Declare that provider as a manifest dependency and avoid using its exports before it has started. Names such as getProtocolVersion are case-sensitive.
Retain callback references when an API requires them for removal. The upstream JavaScript files for RegisterNetEvent, addRawEventListener and removeEventListener are empty at this snapshot; they do not establish return tokens or a safe generic removal recipe. Use a verified runtime API for your target rather than borrowing Lua’s RemoveEventHandler contract. The source-limit inventory records these files explicitly.
Verification boundary
The published server block can be exercised with mocked onNet, on, GetPlayerName, GetGameTimer, emitNet and exports implementations to verify validation, targeting, rate limits and disconnect cleanup. That does not simulate actual network delivery, serialization or Cfx resource teardown. Perform the two-client checks above on the intended track before using the pattern in a real feature.