RescueWidget
Catch exceptions and display a fallback UI, similar to React's ErrorBoundary.
RescueWidget wraps a child widget and catches any exceptions that occur during the widget lifecycle (Reconcile, Measure, Arrange, or Render phases). When an error occurs, it displays a fallback UI instead of crashing the application, allowing users to retry or continue.
Basic Usage
Wrap potentially error-prone content with Rescue:
dotnet runError Boundary Pattern
RescueWidget follows the "error boundary" pattern from React. Each Rescue widget acts as a boundary—errors propagate up until they hit a Rescue widget, which then displays the fallback instead of its normal content.
Wrapping Content
Single Widget
Wrap any widget that might throw:
ctx.Rescue(
ctx.SomeWidget() // This widget may throw
)2
3
VStack Shorthand
Rescue provides a convenient shorthand for wrapping multiple widgets in a VStack:
ctx.Rescue(v => [
v.Text("Some content"),
v.Button("Click me").OnClick(_ => DoSomething())
])2
3
4
This is equivalent to:
ctx.Rescue(
ctx.VStack(v => [
v.Text("Some content"),
v.Button("Click me").OnClick(_ => DoSomething())
])
)2
3
4
5
6
Event Handlers
RescueWidget provides event handlers for logging, telemetry, and state management:
dotnet runOnRescue
Called when an exception is caught. Use this for logging or telemetry:
ctx.Rescue(
ctx.SomeWidget()
)
.OnRescue(e => {
logger.LogError(e.Exception, "Error in {Phase}", e.Phase);
})2
3
4
5
6
The RescueEventArgs provides:
| Property | Type | Description |
|---|---|---|
Exception | Exception | The exception that was caught |
Phase | RescueErrorPhase | When the error occurred (Reconcile, Measure, Arrange, Render) |
Widget | RescueWidget | The source widget |
Node | RescueNode | The rescue node |
OnReset
Called after the user triggers a retry (clicks the Retry button). Use this to reset application state:
ctx.Rescue(
ctx.SomeWidget()
)
.OnRescue(e => logger.LogError(e.Exception, "Error caught"))
.OnReset(e => {
logger.LogInformation("User retried after {Phase} error", e.Phase);
ResetApplicationState();
})2
3
4
5
6
7
8
Both handlers support async versions:
.OnRescue(async e => {
await telemetry.TrackExceptionAsync(e.Exception);
})
.OnReset(async e => {
await ResetStateAsync();
})2
3
4
5
6
Custom Fallback UI
WithFallback
Override the default fallback with your own UI using RescueContext:
dotnet runRescueContext
The RescueContext passed to WithFallback extends WidgetContext<VStackWidget> and provides:
| Property/Method | Type | Description |
|---|---|---|
Exception | Exception | The exception that was caught |
ErrorPhase | RescueErrorPhase | When the error occurred |
Reset() | void | Clears error state and retries the child |
Since RescueContext is a widget context, you can use all the standard widget methods:
.WithFallback(rescue => rescue.Border(b => [
b.VStack(v => [
v.Text($"Error during {rescue.ErrorPhase}:"),
v.Scroll(s => s.Text(rescue.Exception.ToString()).Wrap()).Height(10),
v.Button("Retry").OnClick(_ => rescue.Reset())
])
], title: "Error"))2
3
4
5
6
7
Error Phases
Exceptions can occur at different points in the widget lifecycle:
| Phase | When |
|---|---|
Build | During widget construction (inside the builder function) |
Reconcile | During widget tree reconciliation (building nodes from widgets) |
Measure | When calculating widget sizes |
Arrange | When positioning widgets within their bounds |
Render | When drawing to the terminal |
The phase information helps diagnose where problems occur:
.OnRescue(e => {
if (e.Phase == RescueErrorPhase.Render)
{
logger.LogWarning("Render error - may be terminal compatibility issue");
}
})2
3
4
5
6
Production Mode
RescueFriendly
In production, you typically don't want to show stack traces to users. Use RescueFriendly or set ShowDetails = false:
// Shorthand - hides exception details
ctx.RescueFriendly(
ctx.SomeWidget()
)
// Equivalent to:
ctx.Rescue(ctx.SomeWidget()) with { ShowDetails = false }2
3
4
5
6
7
The default fallback adapts based on ShowDetails:
- ShowDetails = true (default in DEBUG): Shows exception type, message, and stack trace
- ShowDetails = false (default in RELEASE): Shows a user-friendly "Something went wrong" message
Theming
RescueWidget uses RescueTheme for styling the default fallback. The fallback is wrapped in a ThemePanelWidget that maps rescue theme elements to standard widget themes.
| Element | Default | Description |
|---|---|---|
BackgroundColor | Dark red | Fallback background |
ForegroundColor | Light red | Text color |
BorderColor | Bright red | Border lines |
TitleColor | White | Border title |
ButtonBackgroundColor | Dark red | Retry button background |
ButtonForegroundColor | Light red | Retry button text |
ButtonFocusedBackgroundColor | Bright red | Focused button background |
ButtonFocusedForegroundColor | White | Focused button text |
The border uses double-line box characters (╔═╗║╚╝) to visually distinguish error states.
Customize via theme:
var theme = Hex1bTheme.Create()
.Set(RescueTheme.BackgroundColor, Hex1bColor.FromRgb(50, 0, 0))
.Set(RescueTheme.BorderColor, Hex1bColor.Yellow);
await using var terminal = Hex1bTerminal.CreateBuilder()
.WithHex1bApp((app, options) =>
{
options.Theme = theme;
return ctx => /* ... */;
})
.Build();
await terminal.RunAsync();2
3
4
5
6
7
8
9
10
11
12
13
Common Patterns
Top-Level Error Boundary
Wrap your entire application to catch any unhandled errors:
await using var terminal = Hex1bTerminal.CreateBuilder()
.WithHex1bApp((app, options) => ctx => ctx.Rescue(v => [
v.Navigator(/* your app routes */)
])
.OnRescue(e => logger.LogCritical(e.Exception, "Unhandled error")))
.Build();
await terminal.RunAsync();2
3
4
5
6
7
8
Component-Level Boundaries
Isolate errors to specific components so the rest of the UI continues working:
ctx.HStack(h => [
h.Border(b => [
b.Rescue(b.SomeUnstableWidget())
], title: "Widget A"),
h.Border(b => [
b.Rescue(b.AnotherWidget())
], title: "Widget B")
])2
3
4
5
6
7
8
9
If Widget A throws, Widget B continues to render normally.
With Async State Reset
Clean up resources or reset state when retrying:
ctx.Rescue(
ctx.DataViewer(data)
)
.OnRescue(async e => {
await connection.CloseAsync();
await logger.LogErrorAsync(e.Exception);
})
.OnReset(async e => {
data = await RefreshDataAsync();
await connection.ReconnectAsync();
})2
3
4
5
6
7
8
9
10
11
Conditional Fallback
Customize fallback based on the error type:
ctx.Rescue(
ctx.NetworkWidget()
)
.WithFallback(rescue => {
if (rescue.Exception is HttpRequestException)
{
return rescue.VStack(v => [
v.Text("Network error - check your connection"),
v.Button("Retry").OnClick(_ => rescue.Reset())
]);
}
// Default for other errors
return rescue.VStack(v => [
v.Text($"Error: {rescue.Exception.Message}"),
v.Button("Retry").OnClick(_ => rescue.Reset())
]);
})2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Related Widgets
- ThemePanelWidget - Used internally to apply rescue theming
- BorderWidget - Used in the default fallback UI
- ScrollWidget - Used to display scrollable stack traces