Error Boundaries in Blazor: Graceful Error Handling

An unhandled exception in a Blazor Server component kills the circuit. In WebAssembly, it can leave the UI in a broken state. Neither is a good experience. Error boundaries let you catch exceptions within a component subtree, display a fallback UI, and optionally recover — without taking down the rest of the application.

The Default Error Experience

Without error boundaries, Blazor shows a generic error bar at the bottom of the page and logs the exception. In Server mode, the circuit may become unresponsive. Consider a dashboard where one widget throws:

razor
<div class="dashboard">
    <RevenueChart />    <!-- Works fine -->
    <OrdersTable />     <!-- Throws an exception -->
    <UserActivity />    <!-- Never renders -->
</div>

Without error handling, the entire dashboard fails. With error boundaries, only the broken widget shows an error.

Basic ErrorBoundary

Wrap any component subtree in an ErrorBoundary:

razor
<div class="dashboard">
    <ErrorBoundary>
        <RevenueChart />
    </ErrorBoundary>

    <ErrorBoundary>
        <OrdersTable />
    </ErrorBoundary>

    <ErrorBoundary>
        <UserActivity />
    </ErrorBoundary>
</div>

When OrdersTable throws, only its error boundary activates. The other widgets continue to work. The default error UI is a simple <div class="blazor-error-boundary"> with no text — you'll want to customise it.

Custom Error Content

Use the ErrorContent and ChildContent render fragments:

razor
<ErrorBoundary>
    <ChildContent>
        <OrdersTable />
    </ChildContent>
    <ErrorContent Context="exception">
        <div class="error-panel">
            <h3>Something went wrong</h3>
            <p>The orders table couldn't load. Please try refreshing.</p>
            @if (Environment.IsDevelopment())
            {
                <details>
                    <summary>Technical details</summary>
                    <pre>@exception.Message</pre>
                </details>
            }
        </div>
    </ErrorContent>
</ErrorBoundary>

The Context parameter gives you access to the Exception object. Show it in development, hide it in production.

Recovering from Errors

Error boundaries have a Recover method that resets the error state and re-renders the child content:

razor
<ErrorBoundary @ref="errorBoundary">
    <ChildContent>
        <OrdersTable />
    </ChildContent>
    <ErrorContent>
        <div class="error-panel">
            <p>Something went wrong.</p>
            <button @onclick="Retry">Try Again</button>
        </div>
    </ErrorContent>
</ErrorBoundary>

@code {
    private ErrorBoundary? errorBoundary;

    private void Retry()
    {
        errorBoundary?.Recover();
    }
}

When Recover is called, the error boundary clears its error state and re-renders the child content. If the underlying issue is transient (a network timeout, a temporary service failure), this gives the user a way to retry without refreshing the page.

Automatic Recovery on Navigation

A common pattern is to recover all error boundaries when the user navigates. This prevents stale error states from persisting across page changes:

MainLayout.razor
@inherits LayoutComponentBase
@inject NavigationManager Navigation

<ErrorBoundary @ref="errorBoundary">
    <ChildContent>
        @Body
    </ChildContent>
    <ErrorContent>
        <div class="error-page">
            <h1>An error occurred</h1>
            <p>Please try navigating to a different page.</p>
        </div>
    </ErrorContent>
</ErrorBoundary>

@code {
    private ErrorBoundary? errorBoundary;

    protected override void OnParametersSet()
    {
        // Recover on every navigation
        errorBoundary?.Recover();
    }
}

This wraps the entire page body in an error boundary that resets whenever the route changes.

Custom Error Boundary Components

For more control, create a custom error boundary by extending ErrorBoundaryBase:

AppErrorBoundary.cs
public class AppErrorBoundary : ErrorBoundaryBase
{
    [Inject]
    private ILogger<AppErrorBoundary> Logger { get; set; } = default!;

    [Inject]
    private NavigationManager Navigation { get; set; } = default!;

    protected override Task OnErrorAsync(Exception exception)
    {
        Logger.LogError(exception, "Unhandled exception caught by error boundary");

        // You could send to an error tracking service here
        // await ErrorTracker.CaptureAsync(exception);

        return Task.CompletedTask;
    }

    protected override void BuildRenderTree(RenderTreeBuilder builder)
    {
        if (CurrentException is null)
        {
            builder.AddContent(0, ChildContent);
        }
        else if (ErrorContent is not null)
        {
            builder.AddContent(1, ErrorContent(CurrentException));
        }
        else
        {
            // Default fallback
            builder.OpenElement(2, "div");
            builder.AddAttribute(3, "class", "app-error");
            builder.AddContent(4, "An unexpected error occurred.");
            builder.CloseElement();
        }
    }
}

Use it like the built-in version:

razor
<AppErrorBoundary>
    <ChildContent>
        <RiskyComponent />
    </ChildContent>
    <ErrorContent Context="ex">
        <p>Error: @ex.Message</p>
    </ErrorContent>
</AppErrorBoundary>

What Error Boundaries Don't Catch

Error boundaries catch exceptions thrown during rendering and lifecycle methods. They do not catch:

For event handler errors, you still need try-catch:

Example.cs
private async Task LoadData()
{
    try
    {
        data = await Api.GetDataAsync();
    }
    catch (HttpRequestException ex)
    {
        errorMessage = "Failed to load data. Please try again.";
        Logger.LogError(ex, "Data load failed");
    }
}

Practical Strategy

Use error boundaries at two levels:

  1. Layout level — a catch-all that prevents the entire application from crashing
  2. Widget level — around individual components that are prone to failure (data-loading components, third-party integrations)

Don't wrap every single component. That adds noise without value. Focus on isolation boundaries — places where a failure in one section shouldn't affect another.