A Lua library for FiveM inspired by Effect that improves data flow and error propagation across resources.
When resource A calls an export from resource B and B throws an error, FiveM often returns a generic message like "an error occurred in resource X" instead of the actual error message. This makes debugging difficult.
fx treats errors as values (like Effect’s Result type) instead of exceptions. When you use fx.invoke (local) or fx.invokeExport (exports), thrown errors are caught and returned as Result values with the actual message, instead of FiveM's generic "an error occurred in resource X".
You can keep using standard Lua error() and assert(); fx does not change how exports behave.
- Raw
exports.resource:name()on wrapped exports: Success returns the raw value; failure returnsResult.err(no throw). Unwrapped exports behave as before. - Direct function calls: Unchanged. Call any function normally; no wrapping required.
fx.invoke(fn, ...): Local functions. Catches throws and returns a Result. Zero overhead when calling the function directly.fx.invokeExport(resource, export, ...): Exports. Same Result-based error handling.fx.wrap(fn): For exports and callbacks. Success returns the raw value; failure returnsResult.err.
- Add
lua-effectto your server'sresourcesfolder. - Add it as a dependency in your resource's
fxmanifest.lua:
fx_version 'cerulean'
game 'gta5'
dependencies {
'lua-effect',
}
shared_scripts {
'@lua-effect/fx.lua',
}- Use
fx(global) or inline require:
local fx = require('@lua-effect.fx')Optional: ox_lib is only required if you use server ↔ client callbacks with fx.wrap. Add ox_lib to dependencies and @ox_lib/init.lua to shared_scripts in those resources.
Wrap with fx.wrap so errors propagate as values instead of throwing:
-- In your database resource
exports('getUser', fx.wrap(function(userId)
local user = GetUserFromDb(userId)
assert(user, 'User not found: ' .. tostring(userId))
return user
end))Option 1: Return value or throw (same behavior as direct call, but with correct error messages):
local user = fx.invokeExportUnwrap('my-database', 'getUser', 123)
-- If the export fails, you get the real error: "User not found: 123"Option 2: Handle the result explicitly:
local result = fx.invokeExport('my-database', 'getUser', 123)
if fx.isOk(result) then
print('User:', result.value.name)
else
print('Error:', result.error)
endSame function, call directly or via fx.invoke — no wrapping, no overhead:
local function loadConfig(path)
local data = LoadResourceFile(GetCurrentResourceName(), path)
assert(data, 'Config not found: ' .. path)
return json.decode(data)
end
-- Direct call: normal return/throw
local config = loadConfig('config.json')
-- Via fx.invoke: Result with proper error message
local result = fx.invoke(loadConfig, 'config.json')
if fx.isOk(result) then
use(result.value)
endLocal functions can call each other via fx.invoke / fx.invokeUnwrap:
local function validateId(id)
assert(id and id > 0, 'Invalid ID')
return id
end
local function fetchUser(id)
local validId = fx.invokeUnwrap(validateId, id)
return { id = validId, name = 'User ' .. validId }
end
-- Export uses nested local calls
exports('getUser', fx.wrap(function(userId)
return fx.invokeUnwrap(fetchUser, userId)
end))Requires ox_lib. Use fx.wrap for handlers and fx.invoke when awaiting:
-- Server: lib.callback.register with fx.wrap
lib.callback.register('myresource:getPlayerData', fx.wrap(function(source, playerId)
assert(playerId, 'Player ID required')
return GetPlayerData(playerId)
end))
-- Client: lib.callback.await (wrap with fx.invoke for Result)
local result = fx.invoke(lib.callback.await, 'myresource:getPlayerData', false, playerId)| Function | Description |
|---|---|
fx.ok(value) |
Create a success result |
fx.err(message) |
Create a failure result |
fx.isResult(result) |
Returns true if value is a tagged fx Result |
fx.isOk(result) |
Returns true if result is success |
fx.isErr(result) |
Returns true if result is failure |
fx.unwrap(result) |
Return value or throw with the error message |
fx.assertResult(result, msg?) |
Like assert: return value or throw (optional custom message) |
fx.unwrapOr(result, default) |
Return value or default on failure |
fx.map(result, fn) |
Apply fn to the value if success |
fx.mapErr(result, fn) |
Transform the error if failure |
fx.andThen(result, fn) |
Chain: if success, run fn(value) and return its result |
fx.orElse(result, fn) |
Recover: if failure, run fn(error) and return its result |
| Function | Description |
|---|---|
fx.try(fn) |
Wrap a function so it returns a Result instead of throwing |
fx.tryCall(fn) |
Run a function once and return a Result |
| Function | Description |
|---|---|
fx.wrap(fn) |
Wrap for exports/callbacks; success returns raw value, failure returns Result.err |
fx.invoke(fn, ...) |
Call a local function, return Result |
fx.invokeUnwrap(fn, ...) |
Call a local function, return value or throw on error |
fx.invokeExport(resource, export, ...) |
Call an export, return Result |
fx.invokeExportUnwrap(resource, export, ...) |
Call an export, return value or throw on error |
Call functions directly for normal behavior; use fx.invoke or fx.invokeExport when you want Result-based error handling. No wrapping required.
| Function | Description |
|---|---|
fx.pipe(value, fn1, fn2, ...) |
Pass value through fn1, then fn2, etc. Short-circuits on Result error. |
fx.pipeWith(fn, fn1, fn2, ...) |
Run fn() first, then pipe its result through the rest |
-- Simple transform chain
fx.pipe(5, function(x) return x * 2 end, function(x) return x + 10 end)
-- With Result: short-circuits on first error
local result = fx.pipe(jsonStr, parseJson, function(d) return getField(d, 'count') end, validatePositive)
-- pipeWith: first step may fail (e.g. load file)
local cfg = fx.pipeWith(loadConfig, getMaxPlayers)Example: example/example_pipeline/.
| Function | Description |
|---|---|
fx.parseError(message) |
Parse error into { message, location?, file?, line? } — message is clean, location is "file:line" |
fx.getErrorInfo(result) |
For failed results, returns parsed { message, location?, file?, line? } or nil |
-- resource-a/server.lua
exports('getPlayerData', function(playerId)
local data = GetData(playerId)
assert(data, 'Player not found')
return data
end)
-- resource-b/server.lua
local data = exports['resource-a']:getPlayerData(123) -- Generic error on failure-- resource-a/server.lua
exports('getPlayerData', fx.wrap(function(playerId)
local data = GetData(playerId)
assert(data, 'Player not found')
return data
end))
-- resource-b/server.lua
local data = fx.invokeExportUnwrap('resource-a', 'getPlayerData', 123) -- Real error messagelocal result = fx.tryCall(function()
local json = LoadResourceFile(GetCurrentResourceName(), 'data.json')
return json.decode(json)
end)
if fx.isOk(result) then
use(result.value)
else
print('Parse failed:', result.error)
endlocal result = fx.tryCall(function()
return GetUserInput()
end)
local processed = fx.andThen(result, function(user)
return fx.tryCall(function()
return validateAndSave(user)
end)
end)MIT