Skip to content

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:

csharp

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

csharp
ctx.Rescue(
    ctx.SomeWidget() // This widget may throw
)

VStack Shorthand

Rescue provides a convenient shorthand for wrapping multiple widgets in a VStack:

csharp
ctx.Rescue(v => [
    v.Text("Some content"),
    v.Button("Click me").OnClick(_ => DoSomething())
])

This is equivalent to:

csharp
ctx.Rescue(
    ctx.VStack(v => [
        v.Text("Some content"),
        v.Button("Click me").OnClick(_ => DoSomething())
    ])
)

Event Handlers

RescueWidget provides event handlers for logging, telemetry, and state management:

csharp

OnRescue

Called when an exception is caught. Use this for logging or telemetry:

csharp
ctx.Rescue(
    ctx.SomeWidget()
)
.OnRescue(e => {
    logger.LogError(e.Exception, "Error in {Phase}", e.Phase);
})

The RescueEventArgs provides:

PropertyTypeDescription
ExceptionExceptionThe exception that was caught
PhaseRescueErrorPhaseWhen the error occurred (Reconcile, Measure, Arrange, Render)
WidgetRescueWidgetThe source widget
NodeRescueNodeThe rescue node

OnReset

Called after the user triggers a retry (clicks the Retry button). Use this to reset application state:

csharp
ctx.Rescue(
    ctx.SomeWidget()
)
.OnRescue(e => logger.LogError(e.Exception, "Error caught"))
.OnReset(e => {
    logger.LogInformation("User retried after {Phase} error", e.Phase);
    ResetApplicationState();
})

Both handlers support async versions:

csharp
.OnRescue(async e => {
    await telemetry.TrackExceptionAsync(e.Exception);
})
.OnReset(async e => {
    await ResetStateAsync();
})

Custom Fallback UI

WithFallback

Override the default fallback with your own UI using RescueContext:

csharp

RescueContext

The RescueContext passed to WithFallback extends WidgetContext<VStackWidget> and provides:

Property/MethodTypeDescription
ExceptionExceptionThe exception that was caught
ErrorPhaseRescueErrorPhaseWhen the error occurred
Reset()voidClears error state and retries the child

Since RescueContext is a widget context, you can use all the standard widget methods:

csharp
.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"))

Error Phases

Exceptions can occur at different points in the widget lifecycle:

PhaseWhen
BuildDuring widget construction (inside the builder function)
ReconcileDuring widget tree reconciliation (building nodes from widgets)
MeasureWhen calculating widget sizes
ArrangeWhen positioning widgets within their bounds
RenderWhen drawing to the terminal

The phase information helps diagnose where problems occur:

csharp
.OnRescue(e => {
    if (e.Phase == RescueErrorPhase.Render)
    {
        logger.LogWarning("Render error - may be terminal compatibility issue");
    }
})

Production Mode

RescueFriendly

In production, you typically don't want to show stack traces to users. Use RescueFriendly or set ShowDetails = false:

csharp
// Shorthand - hides exception details
ctx.RescueFriendly(
    ctx.SomeWidget()
)

// Equivalent to:
ctx.Rescue(ctx.SomeWidget()) with { ShowDetails = false }

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.

ElementDefaultDescription
BackgroundColorDark redFallback background
ForegroundColorLight redText color
BorderColorBright redBorder lines
TitleColorWhiteBorder title
ButtonBackgroundColorDark redRetry button background
ButtonForegroundColorLight redRetry button text
ButtonFocusedBackgroundColorBright redFocused button background
ButtonFocusedForegroundColorWhiteFocused button text

The border uses double-line box characters (╔═╗║╚╝) to visually distinguish error states.

Customize via theme:

csharp
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();

Common Patterns

Top-Level Error Boundary

Wrap your entire application to catch any unhandled errors:

csharp
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();

Component-Level Boundaries

Isolate errors to specific components so the rest of the UI continues working:

csharp
ctx.HStack(h => [
    h.Border(b => [
        b.Rescue(b.SomeUnstableWidget())
    ], title: "Widget A"),

    h.Border(b => [
        b.Rescue(b.AnotherWidget())
    ], title: "Widget B")
])

If Widget A throws, Widget B continues to render normally.

With Async State Reset

Clean up resources or reset state when retrying:

csharp
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();
})

Conditional Fallback

Customize fallback based on the error type:

csharp
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())
    ]);
})

Released under the MIT License.