blazor-gestion-errores-boundary-components

Error Handling in Blazor with ErrorBoundary

  • 5 min

An ErrorBoundary is a component that catches unhandled exceptions in descendant components and displays an alternative interface.

Without handling, an exception can display Blazor’s error interface. In Interactive Server, an unhandled exception can close the circuit because its state can no longer be guaranteed.

Let’s contain predictable failures, show a useful alternative, and log the details without exposing them to the user.

Catching Expected Errors with try-catch

The first line of defense is standard C# defensive programming. If you know an operation is risky (like calling an API), wrap it.

private string? ErrorMessage;

protected override async Task OnInitializedAsync()
{
    try
    {
        Productos = await ProductoService.GetProductosAsync();
    }
    catch (HttpRequestException ex)
    {
        // Network error (API down, 404, 500)
        ErrorMessage = "We couldn't connect to the server. Please try again later.";
        Logger.LogError(ex, "Error fetching products");
    }
    catch (Exception ex)
    {
        // Unexpected error
        ErrorMessage = "A serious error occurred.";
        Logger.LogError(ex, "Unexpected error fetching products");
    }
}
Copied!

In the view, we render conditionally:

@if (!string.IsNullOrEmpty(ErrorMessage))
{
    <div class="alert alert-danger">@ErrorMessage</div>
}
else if (Productos == null)
{
    <p>Loading...</p>
}
else
{
    }
Copied!

This works well for business logic. But, what happens if the error occurs during rendering? If you have a bug in your HTML (e.g., @usuario.Nombre and usuario is null), the try-catch in OnInitialized won’t save you.

Protecting the UI with ErrorBoundary

ErrorBoundary wraps markup and Razor components. If a descendant throws an unhandled exception during its lifecycle, rendering, or event handling, it shows alternative content.

It works just like a try-catch block, but around HTML tags or Razor Components. If any child component throws an exception, the ErrorBoundary catches it and shows an alternative interface instead of breaking the entire application.

Basic Implementation

We can wrap risky areas of our application.

<ErrorBoundary>
    <ChildContent>
        <WidgetClima />
    </ChildContent>
    <ErrorContent>
        <div class="alert alert-warning">
            ⚠️ The weather widget is temporarily unavailable.
        </div>
    </ErrorContent>
</ErrorBoundary>
Copied!

Notice we use two RenderFragments:

  1. ChildContent: The normal content (happy path).
  2. ErrorContent: The fallback content (error path).

Global Implementation in MainLayout

One possible strategy is to wrap the page body in MainLayout.razor. It’s advisable to keep the boundaries as narrow as practical, so a faulty widget doesn’t replace an entire page.

<main>
    <article class="content px-4">

        <ErrorBoundary @ref="errorBoundary">
            <ChildContent>
                @Body
            </ChildContent>
            <ErrorContent>
                <div class="error-page">
                    <h3>😵 Oops! Something went wrong.</h3>
                    <button class="btn btn-primary" @onclick="Recover">
                        Try again
                    </button>
                </div>
            </ErrorContent>
        </ErrorBoundary>

    </article>
</main>

@code {
    private ErrorBoundary? errorBoundary;

    private void Recover()
    {
        // This method resets the component and retries rendering the ChildContent
        errorBoundary?.Recover();
    }
}
Copied!

The Recover() method clears the error state and attempts to render the content again. Call it after an action that might resolve the cause or when navigating to another page; if the same failure persists, it will throw the exception again.

In a Blazor Web App, an ErrorBoundary placed in a static layout only acts during static SSR. To catch errors from interactive events, the boundary must be within the same interactive tree or the application must use global interactivity.

Logging Errors

Catching the error visually is good for the user, but as developers, we need to know what happened.

Blazor integrates with .NET’s Logging system (ILogger).

In Blazor Server: Logs go to the server console (or Application Insights, Serilog, etc., if configured). In Blazor WebAssembly: Logs go to the Browser Developer Console (F12).

@inject ILogger<MiComponente> Logger

@code {
    try { ... }
    catch (Exception ex)
    {
        // This writes to the browser console in WASM
        Logger.LogError(ex, "Critical error processing order {Id}", orderId);
    }
}
Copied!

Detailed Errors During Development

Even if we use ErrorBoundary, sometimes we want to log any occurring exception, even those breaking the circuit, to send them to an external service (like Sentry or Azure App Insights).

In WebAssembly, unhandled exceptions are written to the browser console. For SSR and server circuits, enable details only in development, as they might contain sensitive data:

// Program.cs: detailed errors during SSR
builder.Services.AddRazorComponents(options =>
    options.DetailedErrors = builder.Environment.IsDevelopment());
Copied!

For interactive circuits, you can also set "DetailedErrors": true in appsettings.Development.json.

Exceptions vs Business Errors

It’s important to differentiate:

  1. Exception (Bug/Crash): “NullReferenceException”, “Database Timeout”. This is handled with ErrorBoundary or global try-catch.
  2. Business Error (Validation): “User has insufficient balance”, “Duplicate Email”.

Predictable results, like “insufficient balance” or “duplicate email,” are better represented with an explicit result or a validation message. Reserve exceptions for conditions that genuinely interrupt the normal flow.

  • Bad: throw new Exception("Insufficient balance"); (The ErrorBoundary will hide the page and show “Oops”).
  • Good: Display a <div class="alert">Insufficient balance</div> and let the user correct the action.