38

I would like to be able to catch all unhandled exceptions in one single place building a Blazor single page application. Like using the "Current.DispatcherUnhandledException" in WPF applications.

This question is exclusively about client-side (webassembly) exception handling. I am using Blazor version 3.0.0-preview8.19405.7

I have been searching for a solution, but it seems like it does not exist. On Microsofts documentation (https://learn.microsoft.com/en-us/aspnet/core/blazor/handle-errors?view=aspnetcore-3.0) there is a list of places errors may occur and a walk through on how to handle each one of them. It believe there must be a more bullet proof way to catch all.

TroelsGP
  • 541
  • 1
  • 5
  • 12
  • Blazor has 2 main flavours: server-side and client-side (webassembly). The answers might be different. Which one are you thinking of? – H H Aug 17 '19 at 21:37
  • Client-side only. I was thinking about a single page application in webassembly. I did some research and it seems like there are more examples of exception handling server-side. I will edit my question to be a bit more precise. – TroelsGP Aug 17 '19 at 23:26
  • Related (only related, not dup) https://stackoverflow.com/a/56872615/842935 – dani herrera Aug 18 '19 at 11:31
  • By the way, I am using Blazor version 3.0.0-preview8.19405.7. I will edit my question with this information. – TroelsGP Aug 18 '19 at 12:41

9 Answers9

30

In .NET 6 there is component called ErrorBoundary.

Simple example:

<ErrorBoundary>
   @Body
</ErrorBoundary>

Advanced Example:

 <ErrorBoundary>
    <ChildContent>
          @Body
    </ChildContent>
    <ErrorContent Context="ex">
          @{ OnError(@ex); } @*calls custom handler*@
          <p>@ex.Message</p> @*prints exeption on page*@
    </ErrorContent>
 </ErrorBoundary>

For the global exception handling I see this as an option: Create CustomErrorBoundary (inherit the ErrorBoundary) and override the OnErrorAsync(Exception exception).

Here is the sample of CustomErrorBoundary.

Useful links

user160357
  • 865
  • 10
  • 14
Alamakanambra
  • 5,845
  • 3
  • 36
  • 43
  • 3
    This isn't working for me if the exception is raised from the constructor of a service. – clamchoda Apr 02 '22 at 01:37
  • 4
    It looks like ErrorBoundry has limitations that are not well documented afaik. Like what @clamchoda mentioned and async methods also don't get caught. More info why : https://github.com/dotnet/aspnetcore/issues/27716#issuecomment-732115003 – user160357 May 24 '22 at 09:26
  • 2
    The `Context="ex"` is the piece that I was missing, as that's what allows you access to the exception details. The MS docs are currently missing that info. Thanks! – deadlydog Oct 05 '22 at 22:07
  • ErrorBoundaries have a bug and won't work if there is more than one exception. Thats why its probably suggested, to only use them in "small" scale, not for global catching. see: https://github.com/dotnet/aspnetcore/issues/39814 – somedotnetguy Jan 20 '23 at 13:25
12

This works in v3.2+

using Microsoft.Extensions.Logging;
using System;

namespace UnhandledExceptions.Client
{
    public interface IUnhandledExceptionSender
    {
        event EventHandler<Exception> UnhandledExceptionThrown;
    }

    public class UnhandledExceptionSender : ILogger, IUnhandledExceptionSender
    {

        public event EventHandler<Exception> UnhandledExceptionThrown;

        public IDisposable BeginScope<TState>(TState state)
        {
            return null;
        }

        public bool IsEnabled(LogLevel logLevel)
        {
            return true;
        }

        public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,
            Exception exception, Func<TState, Exception, string> formatter)
        {
            if (exception != null)
            {
                UnhandledExceptionThrown?.Invoke(this, exception);
            }
        }
    }

    public class UnhandledExceptionProvider : ILoggerProvider
    {
        UnhandledExceptionSender _unhandledExceptionSender;

 
        public UnhandledExceptionProvider(UnhandledExceptionSender unhandledExceptionSender)
        {
            _unhandledExceptionSender = unhandledExceptionSender;
        }

        public ILogger CreateLogger(string categoryName)
        {
            return new UnhandledExceptionLogger(categoryName, _unhandledExceptionSender);
        }

        public void Dispose()
        {            
        }

        public class UnhandledExceptionLogger : ILogger
        {
            private readonly string _categoryName;
            private readonly UnhandledExceptionSender _unhandeledExceptionSender;

            public UnhandledExceptionLogger(string categoryName, UnhandledExceptionSender unhandledExceptionSender)
            {
                _unhandeledExceptionSender = unhandledExceptionSender;
                _categoryName = categoryName;
            }

            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
                // Blazor already logs unhandled exceptions to the browser console
                // but, one could pass the exception to the server to log, this is easily done with serilog
                Serilog.Log.Fatal(exception, exception.Message);                             
            }

            public IDisposable BeginScope<TState>(TState state)
            {
                return new NoopDisposable();
            }

            private class NoopDisposable : IDisposable
            {
                public void Dispose()
                {  
                }
            }
        }
    }
}

Add this to Program.cs

var unhandledExceptionSender = new UnhandledExceptionSender();
var unhandledExceptionProvider = new UnhandledExceptionProvider(unhandledExceptionSender);
builder.Logging.AddProvider(unhandledExceptionProvider);
builder.Services.AddSingleton<IUnhandledExceptionSender>(unhandledExceptionSender);

Here is an example project implementing this solution.

jrummell
  • 42,637
  • 17
  • 112
  • 171
Scotty Hudson
  • 139
  • 1
  • 4
  • 2
    Thanks, Scotty! For those looking to have custom UI display instead of relying on 'blazor-error-ui' in index.html, IJSRuntime can also be passed into UnhandledExceptionSender via dependency injection and then into UnhandledExceptionLogger. This allows using javascript to show custom error display. – Jax Oct 29 '20 at 12:51
  • Hi @Jax, do you have an example of this? – Schoof Nov 13 '20 at 15:28
  • Sorry for the delay. Just notice the message today, @schoof. An adjustment on my part regarding the dependency injection in my previous comment. My working example is something like this. In program.cs, assign service provider to a static property e.g . `WebAssemblyHost host = builder.Build(); SomeClass.ServiceProvider = host.Services;` Somewhere in Log method `IJSRuntime jSRuntime = (IJSRuntime)SomeClass.ServiceProvider.GetService(typeof(IJSRuntime)); jSRuntime.InvokeVoidAsync("SomeJavascriptErrorMethod", someErrorMessage);` – Jax Dec 18 '20 at 10:58
  • 1
    Other notes :- 1) for exceptions thrown from async methods, the Log method doesn't seem to be hit for exceptions from async void methods but works for async Task methods. 2) tried other methods to show messages (eg components) in Blazor but failed - probably because the circuit is broken – Jax Dec 18 '20 at 11:08
  • @Jax good point! more info on this https://github.com/dotnet/aspnetcore/issues/27716#issuecomment-729051853 – user160357 May 23 '22 at 17:29
  • 2
    We are now 2022 and almost on .NET 7, is there a better way to handle this yet? – Schoof Aug 26 '22 at 12:49
9

Currently there is no central place to catch and handle client side exceptions.

Here is a quote from Steve Sanderson about it:

So overall, each component must deal with handling its own errors. If you want, you could make your own ErrorHandlingComponentBase to inherit from, and put a try/catch around all the lifecycle methods, and have your own logic for displaying an "oh dear sorry I died" UI on that component if anything went wrong. But it's not a feature of the framework today.

I hope this will change in the future and I believe support should be backed into the framework.

Jim G.
  • 15,141
  • 22
  • 103
  • 166
Postlagerkarte
  • 6,600
  • 5
  • 33
  • 52
  • 2
    Has there been any movement on this? Seems like a really nice capability to have. – Bjorg Jul 08 '20 at 16:29
  • 6
    How do you put a try/catch around the lifecycle method of a child class in the base class? – Vencovsky Nov 18 '20 at 16:50
  • @Vencovsky Have you figured it out? Now I need it too :P – Efe Zaladin Oct 10 '21 at 17:28
  • @Vencovsky you have to create a component that inherits from ErrorHandlingComponentBase which is just a wrapper class for ComponentBase, with all the lifecycle methods overriden with try/catch. So when your custom component calls OnInitialed it will actually call the wrapper method with try/catch block – user160357 May 23 '22 at 17:03
  • @user160357 you don't call methods like `OnInitialed`. you `override` them. so that doesn't work – symbiont Feb 15 '23 at 19:36
  • Any updates for 2023? – niico Jul 30 '23 at 16:58
7

For .NET 5 Blazor Server Side, this post Create Your Own Logging Provider to Log to Text Files in .NET Core worked for me. For my case, I have adapted this to catch unhandled exceptions to write into Azure storage table.

public class ExceptionLoggerOptions
{
    public virtual bool Enabled { get; set; }
}

[ProviderAlias("ExceptionLogger")]
public class ExceptionLoggerProvider : ILoggerProvider
{
    public readonly ExceptionLoggerOptions Options;

    public ExceptionLoggerProvider(IOptions<ExceptionLoggerOptions> _options)
    {
        Options = _options.Value;
    }

    public ILogger CreateLogger(string categoryName)
    {
        return new ExceptionLogger(this);
    }

    public void Dispose()
    {
    }
}

public class ExceptionLogger : ILogger
{
    protected readonly ExceptionLoggerProvider _exceptionLoggerProvider;

    public ExceptionLogger([NotNull] ExceptionLoggerProvider exceptionLoggerProvider)
    {
        _exceptionLoggerProvider = exceptionLoggerProvider;
    }

    public IDisposable BeginScope<TState>(TState state)
    {
        return null;
    }

    public bool IsEnabled(LogLevel logLevel)
    {
        return logLevel == LogLevel.Error;
    }

    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
    {
        if (false == _exceptionLoggerProvider.Options.Enabled) return;

        if (null == exception) return;

        if (false == IsEnabled(logLevel)) return;

        var record = $"{exception.Message}"; // string.Format("{0} {1} {2}",  logLevel.ToString(), formatter(state, exception), exception?.StackTrace);

        // Record exception into Azure Table
    }
}

public static class ExceptionLoggerExtensions
{
    public static ILoggingBuilder AddExceptionLogger(this ILoggingBuilder builder, Action<ExceptionLoggerOptions> configure)
    {
        builder.Services.AddSingleton<ILoggerProvider, ExceptionLoggerProvider>();
        builder.Services.Configure(configure);
        return builder;
    }
}

    public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(webBuilder =>
    {
        webBuilder.UseStaticWebAssets().UseStartup<Startup>();
    }).ConfigureLogging((hostBuilderContext, logging) =>
    {
        logging.AddExceptionLogger(options => { options.Enabled = true; });
    });
Demir
  • 1,787
  • 1
  • 29
  • 42
  • Do you know if there is a way to access the program, its components, session etc. in the Log method? – Rye bread Oct 27 '21 at 10:44
  • 3
    this doesnt work - I still have exceptions with "An exception of type 'System.Exception' occurred in ServerApp.dll but was not handled in user code" – Ulterior Nov 26 '21 at 07:54
2

To access exception you can use built-in ErrorBoundary component and access RenderFragment using Context attribute

<ErrorBoundary> 
    <ChildContent>
        @Body   
    </ChildContent>
    <ErrorContent Context="ex">
        <h1 style="color: red;">Oops... error occured: @ex.Message </h1>
    </ErrorContent>
</ErrorBoundary>
Lanayx
  • 2,731
  • 1
  • 23
  • 35
2

This will catch ALL errors.

App.razor

<ErrorBoundary>
    <Router AppAssembly="@typeof(App).Assembly">
        <Found Context="routeData">
            <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
            <FocusOnNavigate RouteData="@routeData" Selector="h1" />
        </Found>
        <NotFound>
            <PageTitle>Not found</PageTitle>
            <LayoutView Layout="@typeof(MainLayout)">
                <p role="alert">Sorry, there's nothing at this address.</p>
            </LayoutView>
        </NotFound>
    </Router>
</ErrorBoundary>

If you want to customize the message:

<ErrorBoundary>
    <ChildContent>
        ... App
    </ChildContent>
    <ErrorContent Context="errorException">

        <div class="blazor-error-boundary">
            Boom!
        </div>

    </ErrorContent>
</ErrorBoundary>
Brian Parker
  • 11,946
  • 2
  • 31
  • 41
2

Using the CustomErrorBoundary in the above example, and mudblazor. I made a custom error boundary component that displays the error in a snackbar popup.

In case someone else wants to do this.

CustomErrorBoundary.razor

@inherits ErrorBoundary
@inject ISnackbar Snackbar
@if (CurrentException is null)
{
    @ChildContent
}
else if (ErrorContent is not null)
{
    @ErrorContent(CurrentException)
}
else
{
    @ChildContent

        @foreach (var exception in receivedExceptions)
        {
            Snackbar.Add(@exception.Message, Severity.Error);
        }

    Recover();
}

@code {
    List<Exception> receivedExceptions = new();

    protected override Task OnErrorAsync(Exception exception)
    {
        receivedExceptions.Add(exception);
        return base.OnErrorAsync(exception);
    }

    public new void Recover()
    {
        receivedExceptions.Clear();
        base.Recover();
    }
}

MainLayout.razor

@inherits LayoutComponentBase
@inject ISnackbar Snackbar

<MudThemeProvider IsDarkMode="true"/>
<MudDialogProvider />
<MudSnackbarProvider />

<MudLayout>
    <MudAppBar>
        <MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" OnClick="@((e) => DrawerToggle())" />
    </MudAppBar>
    <MudDrawer @bind-Open="@_drawerOpen">
        <NavMenu/>
    </MudDrawer>
    <MudMainContent>
        <CustomErrorBoundary>
            @Body
        </CustomErrorBoundary>
    </MudMainContent>
</MudLayout>

@code {
    bool _drawerOpen = true;

    private void DrawerToggle()
    {
        _drawerOpen = !_drawerOpen;
    }
}
1

In the current Blazor webassembly version all unhandled exceptions are caught in an internal class and written to Console.Error. There is currently no way to catch them in a different way, but Rémi Bourgarel shows a solution to be able to log them and/or take custom actions. See Remi's blog.

Simple logger to route them to an ILogger:

public class UnhandledExceptionLogger : TextWriter
{
    private readonly TextWriter _consoleErrorLogger;
    private readonly ILogger _logger;

    public override Encoding Encoding => Encoding.UTF8;

    public UnhandledExceptionLogger(ILogger logger)
    {
        _logger = logger;
        _consoleErrorLogger = Console.Error;
        Console.SetError(this);
    }

    public override void WriteLine(string value)
    {
        _logger.LogCritical(value);
        // Must also route thru original logger to trigger error window.
        _consoleErrorLogger.WriteLine(value);
    }
}

Now in Program.cs add builder.Services.AddLogging... and add:

builder.Services.AddSingleton<UnhandledExceptionLogger>();
...
// Change end of Main() from await builder.Build().RunAsync(); to:
var host = builder.Build();
// Make sure UnhandledExceptionLogger is created at startup:
host.Services.GetService<UnhandledExceptionLogger>();
await host.RunAsync();
Marcel Wolterbeek
  • 3,367
  • 36
  • 48
0

The only way I have found to capture all errors ( including Async void without try catch implemented ) for Blazor webassembly is to register to AppDomain.CurrentDomain.UnhandledException event

in MainLayout.razor

@code (

 protected override async void OnInitialized()
    {
        base.OnInitialized();
         AppDomain.CurrentDomain.UnhandledException += (sender, e) =>
             {
                 //Call your class that handles error
             };
}
}