ErrorBoundary and Logging in Blazor

2/3/2022

By: Shaun Walker

Recently I was a guest on the Azure DevOps Podcast where the focus of the episode was Blazor and Oqtane. The discussion was completely unscripted and the conversation flowed from one technical topic to the next... faciliated by the host, Jeffrey Palermo, who asked a variety of insightful questions he felt the audience may be interested in exploring. One of the questions was related to the new ErrorBoundary component in Blazor which was introduced in .NET 6.

For those of you who are not familiar with ErrorBoundary, it is a feature inspired by React which provides Blazor developers with a mechanism for catching any unhandled exceptions within their Razor components. If you are interested in reading some more background about this feature you can follow this link for the full design proposal and comments from the Microsoft Blazor team.

The documentation examples provided by Microsoft with .NET 6 on how to use this new component are very simple and straightforward. You basically wrap your Razor UI logic with the component and choose whether or not you wish to implement some of the optional properties. And the component behaves as expected in terms of notifying the user that an exception has occurred, and preventing the application from entering an unrecoverable state.

<ErrorBoundary>
    <ChildContent>
        @* your component markup *@
    </ChildContent>
    <ErrorContent>
        <p class="errorUI">An unexpected error has occurred</p>
    </ErrorContent>
</ErrorBoundary>

The problem with the ErrorBoundary examples are that they do not represent a real world application scenario. A real world application would not want to simply display an exception message to the user. It would want to display a friendly localized message to the user and it would also want to log the detailed exception to the logging subsystem with all of the available context, so that the application administrator is aware that the exception occurred and has the the ability to reproduce and resolve it. And this is where the challenge arises with the ErrorBoundary examples... it is not obvious how you can perform any logging from the component as it does not appear to support any events or asynchronous capabilities.

In order to resolve this issue I actually had to explore the .NET repo on GitHub to find the actual implementation of the ErrorBoundary component. It turns out that ErrorBoundary component inherits from ErrorBoundaryBase which offers some additional extensibility options. Further research led me to some functional tests created by Steve Sanderson which provide useful hints on how to utilize these capabilities in your own applications.

The ErrorBoundary component was cleverly designed with inheritance in mind. Inheritance allows Razor components which inherit from ErrorBoundary to access its public properties, methods, and events. This includes an OnErrorAsync() event. With this additional insight it is actually very easy to perform logging using the ErrorBoundary component (note that this example has been simplified to highlight only the key extensibility points):

@* MyErrorBoundary.razor *@
@inherits ErrorBoundary
@if (CurrentException is null)
{
    @ChildContent
}
else
{
    <div>@_error</div>
}

@code {
    string _error = "";

    protected override Task OnErrorAsync(Exception exception)
    {
        // friendly message should probably be localized
        _error = "An unexpected error has occurred";
        // call logging service here
        return base.OnErrorAsync(exception);
    }

    public new void Recover()
    {
        _error = "";
        base.Recover();
    }
}

which could be used within your own Razor component (instead of the standard ErrorBoundary):

<MyErrorBoundary>
    <ChildContent>
        @* your component markup *@
    </ChildContent>
</MyErrorBoundary>

Please note that there is also an IErrorBoundaryLogger interface that exists within the ErrorBoundary component. This indicates that it is possible to register a concrete implementation using dependency injection, and the component will automatically log the exception. Unfortunately, there are no actual examples available on how to to do this so again it required some exploration of GitHub. The only example I could find was a WebAssemblyErrorBoundaryLogger:

using Microsoft.AspNetCore.Components.Web;
using Microsoft.Extensions.Logging;

namespace Microsoft.AspNetCore.Components.WebAssembly.Services;

internal sealed class WebAssemblyErrorBoundaryLogger : IErrorBoundaryLogger
{
    private readonly ILogger<ErrorBoundary> _errorBoundaryLogger;

    public WebAssemblyErrorBoundaryLogger(ILogger<ErrorBoundary> errorBoundaryLogger)
    {
        _errorBoundaryLogger = errorBoundaryLogger ?? throw new ArgumentNullException(nameof(errorBoundaryLogger));
    }

    public ValueTask LogErrorAsync(Exception exception)
    {
        _errorBoundaryLogger.LogError(exception, exception.ToString());
        return ValueTask.CompletedTask;
    }
}

Based on the code above it would be possible to create your own custom ErrorBoundaryLogger. The main benefit of this approach is that you could use the standard ErrorBoundary component in your code and also enable custom logging. However, you would not have the same level of flexibility or control offered by the earlier inheritance example so you would need to evaluate your own requirements before choosing a technical approach.

The other more controversial question (actually posed by Jeffrey in the podcast) is if developers should rely on the ErrorBoundary in all of their Razor components... or if they should continue to use traditional Try... Catch blocks within their methods. I can only answer this from my own personal experience developing Oqtane. In general it is better to maintain the precise control and granularity for each exception scenario in your application so that you can provide the most useful guidance to your users and ensure all of the details and context for the exception are captured. This requires the use of traditional Try... Catch blocks. ErrorBoundary should only be used as a fail-safe to protect against those truly unexpected exception conditions which you did not predict during development.



An error has occurred. This application may no longer respond until reloaded. Reload 🗙