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:
<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:
<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:
<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:
<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:
@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:
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:
<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:
- Exceptions in event handlers (like
@onclick) — these are caught by Blazor's event processing pipeline - Exceptions in fire-and-forget
Tasks - Exceptions in JavaScript interop callbacks
- Exceptions during static SSR (these result in HTTP 500 responses)
For event handler errors, you still need try-catch:
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:
- Layout level — a catch-all that prevents the entire application from crashing
- 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.