Error handling

🟨 Slightly different to Blazor Server

Error handling is a critical aspect of building robust and reliable Blazor applications. No matter how carefully you plan and code your application, errors and exceptions are inevitable. However, with the right strategies and tools, you can minimize the impact of errors and quickly diagnose and resolve issues when they arise. In this tutorial:

  • Why error handling is essential?
  • Steps to take when handling an error
  • Types of errors
  • Catching an error
  • The error handler pipeline
  • Actions to take against errors
  • What is an error handler?
  • The need for a custom error handler
  • Building a custom error handler
  • Using a custom error handler
  • Tracking errors
  • Common mistakes
  • Best practices.
  • Key differences between Blazor WebAssembly and Blazor Server.
You can download the example code used in this topic on GitHub.

Why error handling is essential?

  • Improve user experience: When a user encounters an error on your website, whether it's caused by them or the developers, it creates a negative impression. Proper error handling can mitigate the risk of a poor user experience by providing hints to resolve the error, friendly messages, or automatically resolving the error.
  • Enhance website credibility: Errors in production are inevitable, even for global enterprise websites like Google, Microsoft, or Amazon. But, if users encounter error messages like 404, they might assume that your business is not worth trying or a scam website. Handling errors better by providing friendly messages can enhance your website's credibility.
  • Encourage bug reporting: Your loyal users are always willing to provide useful information on any bugs they find on your website. But, 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 and encourage bug reporting.

Steps to take when handling an error

Handling an error involves 2 critical steps:

  1. Error detection: The first step is to detect that something has gone wrong on your website. This can be achieved by implementing proper error handling mechanisms to identify the issue quickly. Additionally, it's essential to answer the question "What went wrong?" accurately.
  2. Appropriate action: Once the error is detected, the next step is to take appropriate action. The minimum action required is to provide a friendly message to the user about the error. However, you can also take additional steps, such as collecting error data for investigation, resuming the operation, or suggesting alternative solutions. Taking the right action can improve User Experience and reduce the likelihood of similar errors occurring in the future.

Types of errors

  1. Server Faults: These types of errors are handled by the host, and Blazor WebAssembly has no control over them. They are usually caused by issues such as network connectivity, server overload, or hardware failure.
  2. HTTP Errors: These types of errors do not directly come from your Blazor WebAssembly website, but from the API side. However, you can catch these errors using Blazor WebAssembly. These errors are usually related to incorrect API requests or server-side issues.
  3. Client-Side Errors: These types of errors are usually caused by components, either by C# code or JavaScript. Blazor WebAssembly allows you to catch these errors and handle them accordingly. They are typically related to issues such as invalid user input, unexpected user behavior, or client-side script errors.

Catching an error

This section will provide an overview of the error catching scopes in Blazor WebAssembly and how to catch errors within each scope.

There are four scopes where you can catch errors:

  1. Global: This scope catches any errors that occur on the website.
  2. Layout: This scope catches any errors thrown from within the layout.
  3. Component: This scope catches any errors thrown from within a component.
  4. HttpClient: This scope catches any errors caused by the API.

Global error catching

When an error hasn't been caught by any other scopes, this is the last fallback to catch it. To make changes, open your wwwroot/index.html file and locate 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 customize the content of this element to show a specific message whenever an error occurs on your website.

Catching errors from layout

In Blazor web development, layouts are essential components defined by @inherits LayoutComponentBase. The default layout, MainLayout.razor, contains the @Body element, which renders child components. By wrapping @Body with the ErrorBoundary component, you can catch and handle all errors that may be thrown by the layout component.

<ErrorBoundary>
    @Body
</ErrorBoundary>

Visit Website Layout to learn more about this crucial aspect of web development.

Catching errors from component

To catch errors that occur within a component, you can use an error handler component. For instance, you can wrap the component that may throw an error with an ErrorBoundary component. Suppose you have a TriggerError component defined as follows:

@inject HttpClient HttpClient

<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");
}

To use an ErrorBoundary component to handle errors thrown by the TriggerError component, you can wrap the TriggerError component inside an ErrorBoundary component in another component, as shown below:

MyComponent.razor

<ErrorBoundary>
    <TriggerError/>
</ErrorBoundary>

Catch API errors with the HttpClient wrapper technique

If you're building an application that consumes an API, it's essential to handle errors gracefully. One way to achieve this is by using the HttpClient wrapper technique. This technique involves wrapping the HttpClient class with a custom class that catches any exceptions thrown by the API and logs them. Visit API Interaction tutorial for more information.

In this example, we have 2 classes:

  • ExceptionRecorderService: Responsible for keeping track of any exceptions that are thrown.
  • BlazorSchoolHttpClientWrapper: This class is a custom HttpClient wrapper that catches exceptions and logs them using the ExceptionRecorderService.
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;
    }
}

To use this technique in your application, register both classes in your Program.cs file using the AddScoped and AddHttpClient methods.

builder.Services.AddScoped<ExceptionRecorderService>();
builder.Services.AddHttpClient<BlazorSchoolHttpClientWrapper>((sp, httpClient) 
    => httpClient.BaseAddress = new(builder.HostEnvironment.BaseAddress));

Once you've registered the classes, you can inject the BlazorSchoolHttpClientWrapper and ExceptionRecorderService into any component and use them to catch and handle API errors.

@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;
    }
}

In the example component provided, we inject the BlazorSchoolHttpClientWrapper and ExceptionRecorderService using the @inject directive. We then use the BlazorSchoolHttpClientWrapper to make an API call that triggers an error. The ExceptionRecorderService catches the error and logs it. Finally, we use the logged error to display an error message to the user.


The error handler pipeline

Blazor comes with an error handling pipeline that facilitates the handling of errors in different scopes based on a predefined sequence. When an unhandled error occurs, the error handling pipeline delegates it to the next scope in sequence.

The first error handler that handles the error is the one in the component scope. If there is no error handler in the component scope, the error is delegated to the layout scope. If there is still no error handler, the error is handled by the global error handler.

If the error is handled by any of the error handlers, it is not delegated to the next scope. However, if it remains unhandled, it continues to delegate to the next scope until it reaches the global error handler.

It's important to note that HTTP request errors are handled in a separate scope. You have the option to handle these errors or choose not to handle them. Unlike other errors, an HttpClient error will not halt your website.

To illustrate the error scope delegation, refer to the following image:

scope-delegation.png


Actions to take against errors

When it comes to handling errors in the second step, there are a couple of common actions you can take. In this section, we'll introduce you to two of them:

  • Displaying an error message to the user.
  • Resuming the operation.

Displaying an error message to the user

Displaying an error message to the user is a common action taken to handle errors in software. When an error occurs, displaying an error message can provide users with information about the error that occurred and how to resolve it. This can help users understand what went wrong and how to fix it, which can improve the user experience and reduce frustration.

The ErrorContent element is a child element of the ErrorBoundary component in Blazor, and it is used to define the content that will be displayed when an error occurs from the child components within the ErrorBoundary scope.

The ErrorContent element accepts a Context parameter, which specifies the name of the variable that will hold the error object that caused the error. You can use this variable to access properties of the error object, such as the error message, and display it to the user.

For example, if you define an ErrorContent element with the Context parameter set to ex, you can access the error message using the @ex.Message syntax.

Here's an example of how to use the ErrorContent element:

<ErrorBoundary>
    <ChildContent>
        <TriggerError/>
    </ChildContent>
    <ErrorContent Context="ex">
        An error occurred: @ex.Message
    ErrorContent>
</ErrorBoundary>

Resuming the operation

In some cases, it may be possible to resume the operation that was interrupted by the error. For example, if an error occurred during a payment transaction, the user may be able to retry the transaction or use an alternative payment method. Resuming the operation can help to minimize the impact of the error on the user experience and ensure that the user can complete the task they were attempting. You can resume the operation by using the ErrorBoundary component and calling its Recover method as follows:

<ErrorBoundary @ref="MyErrorBoundary">
    <ChildContent>
        <TriggerError/>
    </ChildContent>
    <ErrorContent>
        An error occurred. <button @onclick="_ => MyErrorBoundary?.Recover()">Continue</button>
    </ErrorContent>
</ErrorBoundary>

@code {
    public ErrorBoundary? MyErrorBoundary { get; set; }
}

What is an error handler?

An error handler in Blazor is a component that extends either the ErrorBoundary or ErrorBoundaryBase component to handle errors within a specified scope. For instance, if you place an error handler at the component level, it will handle any errors that occur within that component. The ErrorBoundary is the default error handler component, but you can also create custom error handlers.


The need for a custom error handler

In web development, having a custom error handler is crucial for several reasons. While the default error handler can handle most issues, there are instances where a more personalized approach is necessary.

For example, if your website throws more than 100 exceptions, it will be considered critical and fallback to the global error handler. Additionally, you may not have access to the current exception beyond basic details, limiting your ability to respond effectively. By creating a custom error handler, you gain access to the current exception, allowing you to take specific actions such as sending the exception back to your logging API or implementing other solutions.


Building a custom error handler

To create a customized error handler, you can extend either the ErrorBoundary or ErrorBoundaryBase component. However, by extending the ErrorBoundaryBase component, you will have greater flexibility in managing error situations.

To use the ErrorBoundaryBase component effectively, it's important to understand its key features and how they work. Here are the main things to keep in mind:

  1. ChildContent: This is the component that may potentially throw an error. It's important to note that the error must be thrown within ChildContent in order to be caught by the ErrorBoundaryBase component. Refer to the documentation for common mistakes to avoid.
  2. ErrorContent: This is a RenderFragment that will be displayed if an error occurs within ChildContent. It can be customized to provide a better user experience when errors occur.
  3. MaximumErrorCount: This property sets a maximum number of errors that can occur before an error is treated as critical and delegated to the global error handler in Blazor. The default value for MaximumErrorCount in ErrorBoundaryBase is 100.
  4. Recover: This is a method that allows you to reset the current error count and continue using the website without refreshing the page.
  5. OnErrorAsync: This is an abstract method that you must override in your custom ErrorBoundaryBase component. It will be called whenever an error is raised within ChildContent, giving you the opportunity to handle the error in a way that makes sense for your application.

To illustrate this, let's create a sample custom error handler that record a list of exceptions.

  1. To create a custom error handler using the ErrorBoundaryBase component, you should start by creating a new class that extends the ErrorBoundaryBase class. For example:
public class BlazorSchoolErrorBoundary : ErrorBoundaryBase
{
}
  1. Once you have created your custom error handler class, you should override the OnErrorAsync method to record any exceptions that are thrown by the child components. This is where you can add custom logic to handle errors in a way that makes sense for your application. For example:
public class BlazorSchoolErrorBoundary : ErrorBoundaryBase
{
    ...
    public ObservableCollection<Exception> BlazorSchoolExceptions = new();

    protected override Task OnErrorAsync(Exception exception)
    {
        BlazorSchoolExceptions.Add(exception);

        return Task.CompletedTask;
    }
}
  1. Finally, you can override the BuildRenderTree method to display a default custom UI when an error occurs. This is where you can provide a method for the user to resolve the error, if applicable. For example:
public class BlazorSchoolErrorBoundary : ErrorBoundaryBase
{
    ...
    protected void RecoverAndClearErrors()
    {
        Recover();
        BlazorSchoolExceptions.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.AddContent(3, "Blazor School Custom Error Boundary.");
                builder.AddContent(4, __innerBuilder =>
                {
                    __innerBuilder.OpenElement(5, "button");
                    __innerBuilder.AddAttribute(6, "type", "button");
                    __innerBuilder.AddAttribute(7, "onclick", RecoverAndClearErrors);
                    __innerBuilder.AddContent(8, "Continue");
                    __innerBuilder.CloseElement();
                });
                builder.CloseElement();
            }
        }
    }
}

Using a custom error handler

Once you have created your custom error handler, you can use it just like the ErrorHandler component. For instance:

<BlazorSchoolErrorBoundary>
    <ChildContent>
        <TriggerError />
    </ChildContent>
    <ErrorContent>
        <TriggerError />
        <div>Hello Blazor School!</div>
    </ErrorContent>
</BlazorSchoolErrorBoundary>

Tracking errors

It's common for enterprise websites to include an error reporting page, where users can report any issues they encounter to the developers. By utilizing a custom error handler, you can access the CurrentException property, which contains information on the error that occurred. You can then send this object to the error reporting page, allowing you to gather valuable data from the user regarding the issue they faced.


Common mistakes

When handling errors, it's important to avoid common mistakes that can lead to unexpected behavior. In this section, we'll discuss one such mistake.

Throwing an error outside of the error handler's scope

According to data from the Blazor School Discord Community, a common error handling mistake is throwing exceptions outside of the error handler's scope and expecting the error handler to handle the exception. Consider the following code in the TriggerErrorOutsideScope component:

@inject HttpClient HttpClient

<ErrorBoundary>
    <ChildContent>
        <button type="button" @onclick='TriggerHttpClientErrorAsync'>Trigger HttpClient error</button>
        <button type="button" @onclick="TriggerClientSideError">Trigger client side error</button>
    </ChildContent>
    <ErrorContent>An error has occurred.</ErrorContent>
</ErrorBoundary>

@code {
    public async Task TriggerHttpClientErrorAsync() => await HttpClient.GetAsync("https://blazorschool.com");
    public void TriggerClientSideError() => throw new Exception("Blazor School");
}

In this example, there are 2 buttons to trigger an HTTP error and a client-side error. However, neither of these errors will be caught by the ErrorBoundary component, even though they appear to be inside it. This is because the TriggerHttpClientErrorAsync and TriggerClientSideError methods are being executed by the TriggerErrorOutsideScope component, rather than executed inside the ErrorBoundary component.


Best practices

After learning how to handle errors, it's important to keep a few things in mind while developing websites:

  • Use a try-catch block in C# code to handle known errors and take appropriate action. If you cannot take any action against the error, then avoid using the try-catch block and let the error handler do its job.
  • Include an error handler in every layout to catch all unhandled errors and take appropriate action, rather than always delegating to the global error handler and forcing the user to reload the page every time an error occurs.
  • Check for the type of error in the error handler and act accordingly. For example, if the error is from an API (HTTP error), you can use the status code to take necessary action. If there is a 401 Unauthorized error, redirect the user to the login page. If there is a 500 Internal Server Error, send the CurrentException object to your logging API.
  • For all other unhandled errors, send the error details to the logging API. You can use a third-party error provider or build one yourself. By examining these logs, you can make necessary changes to the website.
  • An ideal error report page should be a static HTML page with minimal functionality.

Key differences between Blazor WebAssembly and Blazor Server

In Blazor Server, you have an additional scope to catch first request errors.

BLAZOR SCHOOL
Designed and built with care by our dedicated team, with contributions from a supportive community. We strive to provide the best learning experience for our users.
Docs licensed CC-BY-SA-4.0
Copyright © 2021-2024 Blazor School
An unhandled error has occurred. Reload 🗙