Search code examples
c#exception.net-coreloggingblazor

Show details of uncaught exception in Blazor UI


We have a customer that is using a custom browser, which doesn't allow them to open the console to see the logged error messages. Unfortunately we don't have access to their system so we need to somehow get the exception message and stack trace.

I tried to build a custom ErrorBoundary (https://blazorschool.com/tutorial/blazor-server/dotnet7/error-handling-101402) but the problem is, that this will only either show the error message or the content, even though I render both:

CustomErrorBoundary.cs:

...
    protected override void BuildRenderTree(RenderTreeBuilder builder)
    {
        if (ErrorContent is not null)
        {
                builder.AddContent(0, ErrorContent(CurrentException));
        }
    
        builder.AddContent(1, ChildContent);
    }
...

App.razor:

<CustomErrorBoundary @ref="@errorBoundary">
    <ChildContent>
        <CascadingAuthenticationState>
            <Router AppAssembly="@typeof(App).Assembly">
                <Found Context="routeData">
                    <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
                        <NotAuthorized>
                            <Unauthorized></Unauthorized>
                        </NotAuthorized>
                    </AuthorizeRouteView>
                    <FocusOnNavigate RouteData="@routeData" Selector="h1" />
                </Found>
                <NotFound>
                    <AppPageTitle PageTitle="@Frontend.ErrotPage_NotFoundTitle" />
                    <LayoutView Layout="@typeof(MainLayout)">
                        <MudAlert Severity="Severity.Error" Variant="Variant.Filled" Square="true" Class="ma-2">@Frontend.ErrorPage_NotFound</MudAlert>
                    </LayoutView>
                </NotFound>
            </Router>
        </CascadingAuthenticationState>
    </ChildContent>
    <ErrorContent Context="Exception">
        <p>An error occured.</p>
        @if (Exception != null)
        {
            <p>@Exception.Message</p>
        }
    </ErrorContent>
</CustomErrorBoundary>

What I would need is to extend the default blazor error message ("An unhandled error has occurred. Reload") with a button to expand the exception message and stack trace. As far as I understand that is not possible, because it's in the index.html file.

Is there a way to still show the content and let the user continue using the website, even though there is an exception and also show the exception details?

I know I can setup the logger to send the logs to the API, but there might be exceptions that happen before the connection to the API is there.


Solution

  • I found a solution by extending this answer https://stackoverflow.com/a/63715234/11385442.

    In index.html I added a new element "error-detail":

    <div id="blazor-error-ui">
        An unhandled error has occurred.
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
        <span id="error-detail"></span>
    </div>
    

    I implemented a new logger provider to deal with unhandled exceptions. The UnhandledExceptionLogger is receiving a reference to the UnhandledExceptionProvider which contains an event, that is called whenever an unhandled exception occurs:

    public class UnhandledExceptionProvider : ILoggerProvider
    {
        public event Action<LogLevel, Exception?>? Log;
    
        public UnhandledExceptionProvider()
        {
    
        }
    
        public ILogger CreateLogger(string categoryName)
        {
            return new UnhandledExceptionLogger(this);
        }
    
        public void Dispose()
        {
        }
    
        private class UnhandledExceptionLogger : ILogger
        {
            private readonly UnhandledExceptionProvider unhandledExceptionProvider;
    
            public UnhandledExceptionLogger(UnhandledExceptionProvider unhandledExceptionProvider)
            {
                this.unhandledExceptionProvider = unhandledExceptionProvider;
            }
    
            public bool IsEnabled(LogLevel logLevel)
            {
                return true;
            }
    
            public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
            {
                // Unhandled exceptions will call this method
                unhandledExceptionProvider.OnLog?.Invoke(logLevel, exception);
            }
    
            public IDisposable? BeginScope<TState>(TState state)
                 where TState : notnull
            {
                return new EmptyDisposable();
            }
    
            private class EmptyDisposable : IDisposable
            {
                public void Dispose()
                {
                }
            }
        }
    }
    

    Now in Program.cs I can just use Javascript to set the error details in the DOM:

    public static async Task Main(string[] args)
    {
        ...
        var unhandledExceptionProvider = new UnhandledExceptionProvider();
        builder.Logging.AddProvider(unhandledExceptionProvider);
    
        WebAssemblyHost host = builder.Build();
    
        unhandledExceptionProvider.Log += (LogLevel, exception) =>
        {
            if (logLevel == LogLevel.Critical && exception != null)
            {
                var jsRuntime = host.Services.GetRequiredService<IJSRuntime>();
    
                string stackTrace = exception.StackTrace != null ?
                    Encoding.UTF8.GetString(Encoding.UTF32.GetBytes(exception.StackTrace)) :
                    string.Empty;
                string errorDetail = exception.Message + "<br>" + HttpUtility.HtmlEncode(stackTrace).Replace(@"\", @"\\").Replace("\n", "<br>");
    
                jsRuntime.InvokeVoidAsync("eval", $"document.getElementById('error-detail').innerHTML='{errorDetail}';");
            }
        };
    
        await host.RunAsync();
    }
    

    I also wrote an article about it: https://ben-5.azurewebsites.net/2024/4/17/display-unhandled-client-exceptions-with-blazor/