Skip to content

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

luau
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) -- true

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

luau
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 with Flux.cleanup throws, Flux warns and continues running the remaining cleanups.
  • Runaway Loop Protection: If a self-triggering Effect re-queues itself more than 100 times 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.