Home › Tips
Error Handling
Unhandled errors inside reactive computations or async tasks can interrupt UI updates if not contained. Flux provides Flux.safe for reactive error boundaries, built-in error containment in Flux.async and Effects, and deterministic teardown via Scopes.
Structured Error Handling (Flux.safe)
Flux.safe(tryFn, fallback, equals?) wraps a computation in a self-healing reactive error boundary. It returns a Computed node that evaluates tryFn. If tryFn throws, it returns fallback (either a static value or a function(err) handler):
local HttpService = game:GetService("HttpService")
local rawJson = Flux.signal('{"malformed JSON')
local parsed = Flux.safe(function()
return HttpService:JSONDecode(rawJson())
end, function(err)
warn("JSON parse failed:", err)
return nil
end)
print(parsed()) -- nil (after warning)
-- Self-healing: fixing `rawJson` automatically re-evaluates `parsed`
rawJson('{"ok": true}')
print(parsed().ok) -- trueAny reactive dependencies read before tryFn threw are still tracked, so updating rawJson automatically re-runs parsed and recovers cleanly. The fallback handler itself runs untracked so fallback logging or recovery reads never subscribe the computation to unintended dependencies.
Asynchronous Errors
Flux.async automatically runs its task inside pcall. If the task throws, the error is captured into the .error node and .loading settles to false without crashing the thread:
local userId = Flux.signal(123)
local profile = Flux.async(userId, function(id)
local raw = game:GetService("HttpService"):GetAsync(`https://api.example.com/users/{id}`)
return game:GetService("HttpService"):JSONDecode(raw)
end)
local statusLabel = Flux.new "TextLabel" {
Text = function()
if profile.error() then
return `Error: {profile.error()}`
elseif profile.loading() then
return "Loading..."
end
local data = profile.data()
return if data then data.username else "No data"
end,
}Reactive Graph Errors
Flux isolates failures across the graph so one broken node never corrupts the rest of the frame:
- Computeds: If an unprotected Computed throws, Flux caches the error and re-throws it to the caller reading it while keeping the graph state consistent. When one of the dependencies read before the error changes, the Computed automatically re-evaluates and heals if the new run succeeds.
- Effects: If an Effect throws during a flush, Flux logs a warning (
[Flux] suppressed effect error:) and continues flushing all remaining queued effects. If a cleanup registered withFlux.cleanupthrows, Flux warns and continues running the remaining cleanups. - Runaway Loop Protection: If a self-triggering Effect re-queues itself more than
100times in a single flush, Flux warns and disposes that offending Effect so the frame never hangs.
Scope Safety
Calling :Destroy() on a Scope (or passing items to Flux.dispose) is resilient to errors. Even if one cleanup callback throws, Flux logs a warning and finishes tearing down every remaining instance, connection, and child scope.