Fusion
Fusion copied to clipboard
Error boundaries
Error Boundaries
Error boundaries have been introduced in React. I believe this would be quite helpful for those creating interfaces for bug information and such.
Problem
Currently, there isn't really a way to detect Fusion specific errors properly. I believe the only way to do this is to use ScriptContext or LogService. These solutions are quite hacky anyway, as you could detect errors that aren't actually raised from Fusion.
Implementation
Although I'm not too familiar with the way Fusion handles errors & warnings internally, I believe this could be implemented quite easily because of the files found in Logging.
Additionally, there are different implementations across platforms for this, such as
react-error-boundaryforreact-lua.
Since
react-luais literally an almost 1:1 copy ofReact, they also have implemented error boundaries. You can find more on that here!
Questions
Why would this be useful?
I believe this would be helpful for outfacing interfaces, instead of just failing to render the interface given, it could display an error interface, explaining what happened & giving specific error data for developers. This also introduces the ability to interface error events with Sentry and such.
It's currently very possible to catch errors in Fusion, though not necessarily 'beautiful' - you can parse out info using xpcall (or plain pcall if you don't care about capturing info to do with stack traces or whatnot):
local foo = Computed(function()
local ok, result = pcall(...) -- put your code in here
return {ok = ok, result = result}
end)
Something important to mention is that Fusion is a state management library, not really an instance management library. Fusion does not have enough knowledge to implement features that may be trivial in React. In the case of error boundaries, the reason why I haven't devoted attention to them is that it'd likely require non-trivial special knowledge about the instance hierarchy to be silently propagated in a somewhat magic way. This goes against the Fusion ethos of having rather thin abstractions.
Is there some specific flavour of this feature that better fits in with Fusion's model? Perhaps if the issue is that error handling is not ergonomic in state objects, is there a state-centric solution that better fits this? I would prefer something that works for all data types and use cases rather than limiting ourselves to only Roblox instances.
xpcall() is actually pretty close to the behaviour that's wanted here. In order to catch errors, you'd need to construct children in a callback, and to be able to show a fallback that shows the error message, you'd need to construct that fallback in a callback, too.
The only thing that makes it suboptimal is that it returns a boolean as its first parameter rather than the return value.
So, for convenience, Fusion could likely just drop in a Try function which wraps xpcall so that its return values are more conveniently formatted for child passing.
local function Try<Success, Fail>(
callbacks: {
tryTo: () -> Success,
whenFailed: (err: unknown) -> Fail
}
): Success | Fail
local _, value = xpcall(callbacks.try, callbacks.fallback)
return value
end
This can then be used inline like so:
[Children] = Try {
tryTo = function()
return scope:FallibleComponent {
ThrowError = "You didn't do the right thing :("
}
end,
whenFailed = function(err)
return scope:ErrorMessage {
Text = `Something went wrong: {err}`
}
end
}
By the way, I need opinions on what that function should actually look like / what the name of it should be / what the names of the callbacks should be.
An alternate, more component-centric design using currying:
local function Safe<Scope, Props, Success, Fail>(
scope: Scope,
component: (Scope, Props) -> Success
)
return function(
props: Props & {
OnError: (err: unknown) -> Fail
}
): Success | Fail
local _, value = xpcall(component, props.OnError, scope, props)
return value
end
end
This makes the inline syntax neater, at the cost of looking a bit magical:
[Children] = scope:Safe(scope.FallibleComponent) {
ThrowError = "You didn't do the right thing :(",
OnError = function(err)
return scope:ErrorMessage {
Text = `Something went wrong: {err}`
}
end
}
I think it'll be best to implement this as:
Safe {
try = function()
...
end,
fallback = function(err)
...
end
}