Handling errors properly is essential in building a robust website in Blazor WebAssembly. Error handlers provide a friendly message to the users and allow the developers to collect useful data about an incident. In this tutorial, you will discover:
You can download the example code used in this topic on GitHub.
A lot of programmers often start out using the Console.WriteLine
because that is the default output in most development environments. However, once you deployed your website to a production environment, you no longer have access to the console log. That's because the code is now running on the client browser. Unless you handled the errors to collect errors in a centralized location, you won't have any visibility into them. In order to understand the user experience and how errors can affect it, you need to track errors centrally. That means not only tracking caught errors, but uncaught or run-time errors as well. In order to catch uncaught errors, you need to implement an error handler, which we will describe next.
An error in the production is inevitable, even for global enterprise websites of Google, Microsoft, Amazon… Imagine a user enters your website, provides some information then presses submit, an error page with the message 404 appears. The user might not be a technical person, they don't know what is going on and will assume your business is not worth trying or is a scam website. The situation is totally avoidable if you had handled errors better by providing a friendly message, for example.
Your website might have some loyalty users. Those users are always willing to give you useful information on a bug that they found. However, if you don't provide an easy way to report bugs, you won't get much useful information from them. By providing an error report page, you can collect the error information for investigation.
Handling an error including 2 steps:
There are 3 types of errors:
Keep in mind that any error/exception that comes from the JavaScript code won't automatically caught by default, you need to catch them manually by a try catch
(C# code) block.
You can catch errors by using the ErrorBoundary
component or your custom component. You can catch errors from 4 scopes:
HttpClient
: Catches any error that causes by the API.Open your wwwroot/index.html. You will see the following element:
<div id="blazor-error-ui"> An unhandled error has occurred. <a href="" class="reload">Reload</a> <a class="dismiss">🗙</a> </div>
You can update the content of this element to show a message whenever an error occurs in your website.
A layout is a component with @inherits LayoutComponentBase
. A layout usually put in the Shared folder. You can learn more about layout at Website Layout. In Blazor WebAssembly, the default layout is located at Shared/MainLayout.razor, you will see the following tag in this component:
<article class="content px-4"> @Body </article>
You can use an error handler component to catch errors that come from this layout. For example, we use the ErrorBoundary
component as follows:
<article class="content px-4"> <ErrorBoundary> @Body </ErrorBoundary> </article>
You can catch an error that throws within a component by wrapping the component inside an error handler component. Assuming we have the following TriggerError
component:
@inject HttpClient HttpClient <h3>TriggerError</h3> <button type="button" @onclick='TriggerHttpClientErrorAsync'>Trigger HttpClient error</button> <button type="button" @onclick="TriggerClientSideError">Trigger client side error</button> @code { public async Task TriggerHttpClientErrorAsync() => await HttpClient.GetAsync("https://blazorschool.com"); public void TriggerClientSideError() => throw new Exception("Blazor School"); }
Then in another component, wrap the TriggerError
component inside the ErrorBoundary
component as follows:
<ErrorBoundary> <TriggerError/> </ErrorBoundary>
HttpClient
wrapper techniqueYou can use the HttpClient
wrapper technique to catch any error that causes by the API. Assuming we have the following
ExceptionRecorderService
and BlazorSchoolHttpClientWrapper
class:
public class ExceptionRecorderService { public ObservableCollection<Exception> Exceptions { get; set; } = new(); }
public class BlazorSchoolHttpClientWrapper { private readonly HttpClient _httpClient; private readonly ExceptionRecorderService _exceptionRecorderService; public BlazorSchoolHttpClientWrapper(HttpClient httpClient, ExceptionRecorderService exceptionRecorderService) { _httpClient = httpClient; _exceptionRecorderService = exceptionRecorderService; } public async Task<HttpResponseMessage> GetAsync(string? requestUri) { var response = new HttpResponseMessage(); try { response = await _httpClient.GetAsync(requestUri); } catch (Exception ex) { _exceptionRecorderService.Exceptions.Add(ex); } return response; } }
In the Program.cs
:
builder.Services.AddScoped<ExceptionRecorderService>(); builder.Services.AddHttpClient<BlazorSchoolHttpClientWrapper>((sp, httpClient) => httpClient.BaseAddress = new(builder.HostEnvironment.BaseAddress));
Then you can inject the BlazorSchoolHttpClientWrapper
into any component and use the ExceptionRecorderService
to get the error detail. For example:
@inject ExceptionRecorderService ExceptionRecorderService @inject BlazorSchoolHttpClientWrapper BlazorSchoolHttpClientWrapper @implements IDisposable <h3>HandlingHttpErrorsByHttpClientWrapper</h3> <button @onclick="TriggerHttpErrorAsync">Trigger HTTP Error</button> @foreach (var exception in ExceptionRecorderService.Exceptions) { <div>@exception.Message</div> } @code { protected override void OnInitialized() { ExceptionRecorderService.Exceptions.CollectionChanged += RefreshUI; } public async Task TriggerHttpErrorAsync() { await BlazorSchoolHttpClientWrapper.GetAsync("https://blazorschool.com"); } public void RefreshUI(object? sender, NotifyCollectionChangedEventArgs eventArgs) { InvokeAsync(StateHasChanged); } public void Dispose() { ExceptionRecorderService.Exceptions.CollectionChanged -= RefreshUI; } }
An unhandled error will delegate the handler to the next scope in sequence. An error handler in the component scope will be the first one to handle the error, if there is none, it will be delegated to the layout scope, if there is no error handler in the layout, then the error will be handled by the global error handler. If the error is handled, it will not delegate to the next scope. The HttpClient
error is a standalone scope, you either handle them or not handle them, an HttpClient
error will not halt your website. The following image illustrates the error scope delegation:
Take actions against an error in the second step of handling an error. In this section, we will introduce you 2 common actions against an error, they are:
When an error occurs, the users might not need what is the error, but they certainly need a friendly message to soothe the experience with your website. You can display a friendly message when an error occurs by using the ErrorContent
component as follows:
<ErrorBoundary> <ChildContent> <TriggerError/> </ChildContent> <ErrorContent Context="ex"> Caught an exception from the TriggerError componennt. The exception message is @ex.Message. </ErrorContent> </ErrorBoundary>
An error occur will halt the current operation of the user. You can let the users ignore the error and continue using your website by calling Recover
method of the ErrorBoundary
component as follows:
<ErrorBoundary @ref="ComponentErrorBoundary"> <ChildContent> <TriggerError/> </ChildContent> <ErrorContent Context="ex"> An error has occurred. <button @onclick="_ => ComponentErrorBoundary.Recover()">Continue</button> </ErrorContent> </ErrorBoundary> @code { public ErrorBoundary ComponentErrorBoundary { get; set; } = default!; }
An error handler is a component that extends the ErrorBoundary
or ErrorBoundaryBase
component and be handle any error in a scope. For example, if an error handler is put at the component scope, it will handle any error from within the scope. ErrorBoundary
is the built-in error handler component, you can create your own error handler component as well.
Sometimes, you need to have more control over the handling process. By default, if your website throws more than 100 exceptions, it will be considered as critical and fallback to the global error handler. By default, you don't have access to the current exception (you can show the detail but that's it) and therefore you cannot send the exception back to your logging API. Build a custom error handler provide you the access to the current exception and you can send the exception back to the logging API for example or take other actions.
You can create a custom error handler by creating a component that extends the ErrorBoundary
or ErrorBoundaryBase
component. Extending the ErrorBoundaryBase
gives you more control when handling an error. Here is our example custom error handler:
public class BlazorSchoolErrorBoundary : ErrorBoundaryBase { [Inject] public ExceptionRecorderService ExceptionRecorderService { get; set; } = default!; public BlazorSchoolErrorBoundary() { MaximumErrorCount = 2; } protected override Task OnErrorAsync(Exception exception) { ExceptionRecorderService.Exceptions.Add(exception); return Task.CompletedTask; } protected void RecoverAndClearErrors() { Recover(); ExceptionRecorderService.Exceptions.Clear(); } protected override void BuildRenderTree(RenderTreeBuilder builder) { if (CurrentException is null) { builder.AddContent(0, ChildContent); } else { if (ErrorContent is not null) { builder.AddContent(1, ErrorContent(CurrentException)); } else { builder.OpenElement(2, "div"); builder.AddAttribute(3, "class", "text-danger border border-danger p-3"); builder.AddContent(4, "Blazor School Custom Error Boundary."); builder.AddContent(5, __innerBuilder => { __innerBuilder.OpenElement(6, "button"); __innerBuilder.AddAttribute(7, "type", "button"); __innerBuilder.AddAttribute(8, "class", "btn btn-link"); __innerBuilder.AddAttribute(9, "onclick", RecoverAndClearErrors); __innerBuilder.AddContent(10, "Continue"); __innerBuilder.CloseElement(); }); builder.CloseElement(); } } } }
When implementing a custom error handler, you need to pay attention to:
MaximumErrorCount
: If there is x number of exceptions in the error handler, then it will fall back to the global exception handler. In the example, we set it to 2 in the constructor.OnErrorAsync
: What to do when an error occurs. In the example, we record exception with the ExceptionRecorderService
service in the OnErrorAsync
method.BuildRenderTree
: Render the UI when an error occurs. In the example, we print out the text "Blazor School Custom Error Boundary" and a button to continue the operation.Once you have your custom error handler component, you can use it as the ErrorBoundary
component. For example:
<BlazorSchoolErrorBoundary> <ChildContent> <TriggerError /> </ChildContent> <ErrorContent> <TriggerError /> <div>Throw more error and I will crash.</div> </ErrorContent> </BlazorSchoolErrorBoundary>
<BlazorSchoolErrorBoundary> <TriggerError /> </BlazorSchoolErrorBoundary>
In most of enterprise websites, you will see an error report page for the user to report problems to the developers. With the custom error handler, you can access the CurrentException
property. You can send this object to the report problem page to collect the user's data on a defect.
When handling an error, make sure you don't make the common mistake we are going to talk about in this section.
Based on the data of the Blazor School Discord Community, we see most of the people throw exceptions outside of the error handler's scope and expecting the error handler will handling the exception. Consider the following code in the TriggerErrorOutsideScope
component:
@inject HttpClient HttpClient <h3>TriggerErrorOutsideScope</h3> <ErrorBoundary> <button class="btn btn-primary" type="button" @onclick='TriggerHttpClientErrorAsync'>Trigger HttpClient error</button> <button class="btn btn-primary" type="button" @onclick="TriggerClientSideError">Trigger client side error</button> </ErrorBoundary> @code { public async Task TriggerHttpClientErrorAsync() => await HttpClient.GetAsync("https://blazorschool.com"); public void TriggerClientSideError() => throw new Exception("Blazor School"); }
In the example, there are 2 buttons to trigger an HTTP error and the client side error. Both won't be caught by the ErrorBoundary
component although they appear to be inside the ErrorBoundary
component. That is, because the TriggerHttpClientErrorAsync
and TriggerClientSideError
is being executed by the component TriggerErrorOutsideScope
component rather than executed inside the ErrorBoundary
component.
You have learnt how to handle errors, here are a few things you should keep in mind while developing website:
try catch
block in C# code to handle the know errors. Act accordingly. If you are not able to take action against it, then don't use the try catch
block and let the error handler do the job.CurrentException
object to your logging API.