Unhandled Exceptions in Blazor Server with Error Boundaries

This post originally appeared on the Telerik Developer Blog.

In Blazor—especially Blazor Server—there is no such thing as a small unhandled exception. When Blazor Server detects an unhandled exception from a component, ASP.NET Core treats it as a fatal exception. Why?

Blazor Server apps implement data processing statefully so that the client and server can have a "long-lived relationship." To accomplish this, Blazor Server creates a circuit server-side, which indicates to the browser how to respond to events and what to render. When an unhandled exception occurs, Blazor Server treats it as a fatal error because the circuit hangs in an undefined state, which can potentially lead to usability or security problems.

As a result, your app is as good as dead, loses its state, and your users are met with an undesirable An unhandled error has occurred message, with a link to reload the page.

In many cases, unhandled exceptions can be out of a developer's control—like when you have an issue with third-party code. From a practical perspective, it's not realistic to assume components will never throw.

Here's an example that a Blazor developer posted on GitHub: let's say I've got two components: MyNicelyWrittenComponent, which I wrote, and a third-party component, BadlyWrittenComponent, which I did not write. (Badly written components rarely identify themselves so well, but I digress.)

<div>
   <MyNicelyWrittenComponent>
   <BadlyWrittenComponent Name="null">
</div>

What if the third-party code is coded like this?

@code {
    [Parameter] public string Name { get; set; }
}
<p>@Name.ToLower()</p>

While I can reasonably expect BadlyWrittenComponent to blow up, the circuit is broken, and I can't salvage MyNicelyWrittenComponent. This begs the question: if I get an unhandled exception from a single component, why should my entire app die? As a Blazor developer, you're left spending a lot of time hacking around this by putting try and catch blocks around every single method of your app, leading to performance issues—especially when thinking about cascading parameters.

If you look at our front-end ecosystems, like React, they use Error Boundaries to catch errors in a component tree and display a fallback UI when a failure occurs for a single component. When this happens, the errors are isolated to the problematic component, and the rest of the application remains functional.

Fear no more: with .NET 6 Preview 4, the ASP.NET Core team introduced Blazor error boundaries. Inspired by Error Boundaries in React, it attempts to catch recoverable errors that can't permanently corrupt state—and like the React feature, it also renders a fallback UI.

From the Error Boundaries design document, the team notes that its primary goals are to allow developers to provide fallback UIs for component subtrees if an exception exists, allow developers to provide cleaner fallback experiences, and provide more fine-grained control over how to handle failures. This capability won't catch all possible exceptions but most common scenarios such as the various lifecycle methods (like OnInitializedAsync, OnParametersSetAsync, and OnAfterRenderAsync , and rendering use cases with BuildRenderTree.

In this post, I'll help you understand what Error Boundaries are—and just as importantly, what they aren't.

Get Started: Add Error Boundaries to the Main Layout

To get started, let's use a quick example. Let's say I know a guy—let's call him MAUI Man—that runs a surf shop. In this case, MAUI Man has a Blazor Server app where he sells surfboards and t-shirts. His developer, Ed, wrote a ProductService that retrieves this data.

Let's say the Surfboard API is having problems. Before the error boundary functionality was introduced, my component would fail to process the unhandled exception, and I'd see the typical error message at the bottom of the page.

Let's use the new ErrorBoundary component instead. To do this, navigate to the MainLayout.razor file. (If you aren't familiar, the MainLayout component is your Blazor app's default layout.) In this file, surround the @Body declaration with theErrorBoundary component. If an unhandled exception is thrown, we'll render a fallback error UI.

@inherits LayoutComponentBase

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <div class="main">
        <div class="content px-4">
          <ErrorBoundary>
            @Body
          </ErrorBoundary>    
        </div>
    </div>
</div>

On the home page where we call the Surfboard API, we see the default error UI.

The default UI does not include any content other than An error has occurred. Even in development scenarios, you won't see stack trace information. Later in this post, I'll show you how you can include it.

This UI renders an empty <div> with a blazor-error-boundary CSS class. You can override this in your global styles if desired, or even replace it altogether. In the following example, I can customize my ErrorBoundary component. In this example, I include the @Body inside a RenderFragment called ChildContent. This content displays when no error occurs. Inside ErrorContent—which, for the record, is technically a RenderFragment<Exception>—displays when there's an unhandled error.

<ErrorBoundary>
  <ChildContent>
     @Body
  </ChildContent>
  <ErrorContent>
     <p class="my-custom-class">Whoa, sorry about that! While we fix this problem, buy some shirts!</p>
  </ErrorContent>
</ErrorBoundary>

How else can you work with the ErrorBoundary? Let's explore.

Exploring the ErrorBoundary Component

Aside from the ChildContent and ErrorContent fragments, the out-of-the-box ErrorBoundary component also provides a CurrentException property—it's an Exception type you can use to get stack trace information. You can use this to add to your default error message (which should only be exposed in development environments for security reasons).

Most importantly, the ErrorBoundary component allows you to call a Recover method, which resets the error boundary to a "non-errored state." By default, the component handles up to 100 errors through its MaximumErrorCount property. The Recover method does three things for you: it resets the component's error count to 0, clears the CurrentException and calls StateHasChanged. The StateHasChanged call notifies components that the state has changed and typically causes your component to be rerendered.

We can use this to browse to our Shirts component when we have an issue with the Surfboard API (or the other way around). Since the boundary is set in the main layout, at first we see the default error UI on every page. We can use the component's Recover method to reset the error boundaries on subsequent page navigations.

@code {
    ErrorBoundary errorBoundary;

    protected override void OnParametersSet()
    {
        errorBoundary?.Recover();
    }
}

As a best practice, you'll typically want to define your error boundaries at a more granular level. Armed with some knowledge about how the ErrorBoundary component works, let's do just that.

Dealing with Bad Data

It's quite common to experience issues with getting and fetching data, whether it's from a database directly or from APIs we're calling (either internal or external APIs). This can happen for a variety of reasons: from a flaky connection, an API's breaking changes, or just inconsistent data.

In my ShirtList component, we call off to a ShirtService. Here's what I have in ShirtList.razor.cs:

using ErrorBoundaries.Data;
using Microsoft.AspNetCore.Components;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace ErrorBoundaries.Pages
{
    partial class ShirtList : ComponentBase
    {
        public List<Shirt> ShirtProductsList { get; set; } = new List<Shirt>();

        [Inject]
        public IProductService ShirtService { get; set; }

        protected override async Task OnInitializedAsync()
        {
            ShirtProductsList = await ShirtService.GetShirtList();
        }
    }
}

In my ShirtList.razor file, I'm iterating through the ShirtProductsList and displaying it. (If you're worked in ASP.NET Core before you've done this anywhere between five million and ten million times.)

<table class="table">
 <thead>
     <tr>
         <th>ID</th>
         <th>Name</th>
         <th>Color</th>
         <th>Price</th>
     </tr>
  </thead>
  <tbody>
     @foreach (var board in ShirtProductsList)
     {  
         <tr>
            <td>@board.Id</td>
            <td>@board.Name</td>
            <td>@board.Color</td>
            <td>@board.Price</td>
          </tr>   
        }
  </tbody>
</table>

Instead, I can wrap the display in an ErrorBoundary and catch the error to display a message instead. (In this example, to avoid repetition I'll show the <tbody> section for the Razor component.)

<tbody>
   @foreach (var board in ShirtProductsList)
   {
        <ErrorBoundary @key="@board">
            <ChildContent>
                <tr>
                   <td>@board.Id</td>
                   <td>@board.Name</td>
                   <td>@board.Color</td>
                   <td>@board.Price</td>
                 </tr>   
             </ChildContent>
             <ErrorContent>
                 Sorry, I can't show @board.Id because of an internal error.
             </ErrorContent>
         </ErrorBoundary>        
     }
</tbody>

In this situation, the ErrorBoundary can take a @key, which in my case is the individual Surfboard, which lives in the board variable. The ChildContent will display the data just as before, assuming I don't get any errors. If I encounter any unhandled errors, I'll use ErrorContent to define the contents of the error message. In my case, it provides a more elegant solution than placing generic try and catch statements all over the place.

What Error Boundaries Isn't

I hope this gentle introduction to Blazor Error Boundaries has helped you understand how it all works. We should also talk about which use cases aren't a great fit for Error Boundaries.

Blazor Error Boundaries is not meant to be a global exception handling mechanism for any and all unhandled errors you encounter in your apps. You should be able to log all uncaught exceptions using the ILogger interface.

While you can mark all your components with error boundaries and ignore exceptions, you should take a more nuanced approach to your application's error handling. Error Boundaries are meant for control over your specific component's unhandled exceptions and not a quick way to manage failures throughout your entire application.

Conclusion

While it's been nice to see the growth of Blazor over the several years, it's also great to see how the ASP.NET Core team isn't afraid to add new features by looking around at other leading front-end libraries—for example, the CSS isolation was inspired by Vue and Error Boundaries is taking cues from what React has done. Not only is Blazor built on open web technologies, but it's also not afraid to look around the community to make things better.

Do you find this useful? Will it help you manage component-specific unhandled exceptions? Let us know in the comments.

39