Error boundaries¶
Errored catches errors raised while rendering its
subtree and shows a fallback in its place, leaving sibling trees
untouched. It's the recommended way to surface unexpected errors
without crashing the whole app.
from wybthon import Errored, component
from wybthon.html import button, div, p
@component
def Failing():
raise RuntimeError("boom")
def fallback(err, reset):
return div(
p(t"Oops: {err}"),
button("Try again", on_click=reset),
class_="error",
)
view = Errored(lambda: Failing(), fallback=fallback)
children may be a VNode, a zero-arg callable, or a list of either.
Passing a callable defers building the subtree until the boundary
mounts, which is what you want when the children can raise.
The fallback receives the error as an accessor, as in Solid 2.0:
call err() to read the exception, or interpolate err in a
t-string, as above, to show its message. reset takes no arguments, so it can be passed
straight to on_click.
What gets caught¶
The boundary installs an error handler on its owner scope. Errors from any of these route to the nearest enclosing boundary:
- A component body raising during mount.
- A hole's expression or a reactive prop binding raising when it (re-)evaluates.
- An effect's compute stage raising, unless the effect has its own
error=handler. - An async memo rejecting: the error is stored and re-raised when the memo is read, so it surfaces in the hole that reads it.
- An
actionraising: routed to the boundary that was active when the action was called, and re-raised to the awaiter.
The nearest boundary wins. An inner Errored handles errors from its
own subtree; only errors outside it reach the outer one:
Errored(
lambda: div(
Sidebar(),
Errored(lambda: RiskyPanel(), fallback=lambda err: p("Panel failed")),
),
fallback=lambda err: p("Page failed"),
)
Fallback shapes¶
fallback may be:
- a
VNodeor a string, shown as is; - a callable
() -> VNode; - a callable
(err) -> VNode, whereerris an accessor for the caught exception; - a callable
(err, reset) -> VNode, wherereset()clears the error and re-renders the children; - omitted, in which case the boundary renders the text "Something went wrong."
A callable fallback runs each time an error is caught, so it can inspect
the exception by calling err():
from wybthon.html import button, div, p
def describe(err, reset):
if isinstance(err(), PermissionError):
return p("You don't have access to this.")
return div(p(lambda: str(err())), button("Retry", on_click=reset))
Resetting¶
Call the reset argument from the fallback, or pass reset_on=: an
accessor (or plain value) whose change clears the current error
automatically. Coupling a boundary to the current route is the usual
pattern, so navigating away from a broken page recovers on its own:
from wybthon import Errored
from wybthon.html import p
from wybthon.router import Outlet, current_path
Errored(lambda: Outlet(), fallback=lambda err: p("This page failed"), reset_on=current_path)
Resetting re-renders the children. If the cause hasn't been fixed, the error is caught again and the fallback returns.
Automatic healing¶
A boundary also resets on its own when any input the failing
computation read changes. The boundary records the signals behind
the memo or hole that raised (the user_id a fetch read, the store
path a hole formatted) and watches them; a change to one of them is a
new situation, so the boundary re-renders the children without you
wiring reset_on=. A fetch that failed for user 3 heals when the user
picks user 4, and an async memo that errored heals when its inputs
change and the retry succeeds. Explicit reset() and reset_on= still
work for causes the graph can't see, such as a network that came back.
Observing errors with on_error¶
Pass on_error= to be notified when the boundary catches something, for
logging or monitoring. It receives the exception itself (not an
accessor) and runs in addition to showing the fallback:
Effects with their own handler¶
create_effect accepts error=. Exceptions
raised by the compute stage (sync or async) go to that handler instead
of the nearest boundary, so a failing background effect doesn't take
down the UI around it:
from wybthon import create_effect
create_effect(
lambda: save_draft(draft()),
lambda result: set_status("saved"),
error=lambda exc: set_status(f"save failed: {exc}"),
)
Event handlers are not routed¶
Like SolidJS, Wybthon runs event handlers outside rendering. An
exception in an on_click handler is logged to the console (with a
traceback in dev mode); the boundary isn't involved and the UI stays
intact. Handle expected failures inside the handler, or move the work
into an action, whose errors are routed to the
boundary captured at call time.
Pairing with Loading¶
Errored handles errors; Loading handles
not-ready state. Wrap async regions with both, Errored on the outside
so a rejected fetch inside the loading content still has a fallback:
from wybthon import Errored, Loading
from wybthon.html import button, div, p
Errored(
lambda: Loading(lambda: UserCard(user_id=user_id), fallback=p("Loading...")),
fallback=lambda err, reset: div(p(t"Could not load: {err}"), button("Retry", on_click=reset)),
)
Next steps¶
- See the
error_boundaryAPI reference. - Read Async and loading for async error handling.
- Walk through the Error boundary example.