Error handling

Error handling refers to the response and recovery procedures from error conditions present in website development. In other words, it is the process comprised of anticipation, detection, and resolution of application errors, programming errors or communication errors. Error handling helps in maintaining the normal flow of program execution. In fact, many websites face numerous design challenges when considering error-handling techniques. In this tutorial, we will walk you through this crucial technique with the following content:

  • What is error handling?
  • Why do you need to handle errors?
  • What to do to handle an error?
  • The main categories of errors.
  • Catching an error.
  • Error handler pipeline.
  • Take actions against an error.
  • What is an error handler?
  • Why do you need a custom error handler?
  • Build a custom error handler.
  • Using custom error handler.
  • Tracking error.
  • Common mistakes.
  • Best practice.
You can download the example code used in this topic on GitHub.

What is error handling?

Error handling helps in handling both user errors and programming errors gracefully and helps execution to resume when interrupted. When it comes to error handling in website development, the programmers develop the necessary codes to handle errors if it is recoverable or, at least, show a friendly message to tell the users what to do or what went wrong. In cases where errors cannot be classified, error handling is usually done with returning special error codes along with some environment information to help the programmers investigate and improve the website.


Why do you need to handle errors?

  • Mitigate the risk of a poor User Experience.

Whenever a user encounters an error, whether it came from the user or came from the developers. The user already has a negative feeling about your website. To mitigate the risk of a poor User Experience, nothing better than to handle expected errors and even… unexpected errors. By giving the hints to resolve the error, a friendly message, or you resolve the error automatically. It all helps improve the User Experience.

  • Website credibility.

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.

  • Free bug hunters!

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.


What to do to handle an error?

Handling an error including 2 steps:

  1. Catching the error: In this step, detecting something went wrong in your website is the least you can do. You can also improve this step by able to answer the question “What went wrong?”.
  2. Act accordingly: In this step, you give the user a friendly message about the error is the least you can do. You can also improve this step by collect the error data or resume the operation and more.

The main categories of errors

There are 3 categories of errors:

  1. Server Faults: This type of error caused by wrong settings in the host. Blazor Server can do nothing about this.
  2. HTTP Errors: If your website has some interactions with a third party API, the HTTP might sometimes return error like 401, 404,...
  3. Client Side Errors: This type of error usually come from components. Sometimes by C# code, sometimes by JavaScript.

Catching an error

You can catch an error in 5 scopes:

  1. First request error page: Catches errors when Blazor is failed to initiate.
  2. Global: If Blazor is successfully initiated, this is the last fallback to handle any errors.
  3. Layout: If Blazor is successfully initiated, this scope will catch any error thrown from within the layout.
  4. Component: If Blazor is successfully initiated, this scope will catch any error thrown from within the component.
  5. HttpClient: Catches any unsuccessfully request with status code different than 200. Not necessary an error.
Take actions against an error Access HttpContext
First request error page Provide multiple actions Yes
Global Show friendly message No
Layout Provide multiple actions No
Component Provide multiple actions No
HttpClient Provide multiple actions No

First request error catching

Blazor is an SPA framework, when the first request fails, it falls to the first request error page to handle the error. Typically, accessing the HttpContext in Blazor should be avoided at all costs. However, because the Blazor is failed to initiate, so you can access the HttpContext to get more information and investigate the problem. The default first request error page is Error.cshtml with the path "/Error". You can create your custom page or modify this page as needed.

  1. Set the path to the first request error page in Program.cs. For example:
app.UseExceptionHandler("/BlazorSchoolError");
  1. Create a new Razor Page (and not Razor Component).
@page "/BlazorSchoolError"
@model ErrorHandling.Pages.ErrorModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>Error</title>
    <link href="~/css/bootstrap/bootstrap.min.css" rel="stylesheet" />
    <link href="~/css/site.css" rel="stylesheet" asp-append-version="true" />
</head>

<body>
    <div class="main">
        <div class="content px-4">
            An error has occurred. Press "Submit" to report the error.
            <form method="post">
                <button type="submit">Submit</button>
            </form>
        </div>
    </div>
</body>

</html>
  1. Create the code behind class. The code behind class name must match with the @model at step 2. For example:
namespace ErrorHandling.Pages;
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
    public string? RequestId { get; set; }

    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);

    private readonly ILogger<ErrorModel> _logger;

    public ErrorModel(ILogger<ErrorModel> logger)
    {
        _logger = logger;
    }

    public void OnGet() => RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;

    public void OnPost()
    {
        var exceptionHandler = HttpContext.Features.Get<IExceptionHandlerFeature>();

        if (exceptionHandler is not null)
        {
            Console.WriteLine($"You can log the error with the detailed message in the exceptionHandler {exceptionHandler.Error.Message}");
            Console.WriteLine($"You can log the error with the stack trace in the exceptionHandler {exceptionHandler.Error.StackTrace}");
        }
    }
}

Global error catching

This is the last fallback to handle errors when Blazor is initiated. You can display different messages on different environments. This helps prevent any detailed information leak to the user. In some exceptional cases, you need to reveal detailed information to see what's wrong with your website, but you need to hide it right after you have gathered your information.

In _Layout.cshtml, you can find the following HTML tag:

<div id="blazor-error-ui">
    <environment include="Staging,Production">
        An error has occurred. This application may no longer respond until reloaded.
    </environment>
    <environment include="Development">
        An unhandled exception has occurred. See browser dev tools for details.
    </environment>
    <a href="" class="reload">Reload</a>
    <a class="dismiss">??</a>
</div>

You can update the content of this HTML tag to show a message whenever an error occurs in your website.

Catching errors from layout

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 Server, 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 built-in ErrorBoundary component as follows:

<article class="content px-4">
    <ErrorBoundary>
        @Body
    </ErrorBoundary>
</article>

Component error catching

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 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>

@code {
    public async Task TriggerHttpClientErrorAsync() => await HttpClient.GetAsync("https://unknown-wesite-2234123.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>

Catching HTTP errors by HttpClient wrapper technique

You 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>();

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://unknown-wesite-2234123.com");
    }

    public void RefreshUI(object? sender, NotifyCollectionChangedEventArgs eventArgs)
    {
        InvokeAsync(StateHasChanged);
    }

    public void Dispose()
    {
        ExceptionRecorderService.Exceptions.CollectionChanged -= RefreshUI;
    }
}

Error handler pipeline

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. HTTP request errors are in a seperately scope, you either handle them or not handle them, an HttpClient error will not halt your website. The first request error will halt your website. The following image illustrates the error scope delegation:

scope-delegation.png


Take actions against an error

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:

  1. Display the error message to the user.
  2. Resume the operation.

Display the error message to the user

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>

Resume the operation

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">
        Caught an exception from the TriggerError componennt. The exception message is <span >@ex.Message</span>. 
<button @onclick="_ => ComponentErrorBoundary?.Recover()">Continue</button>
    </ErrorContent>
</ErrorBoundary>

@code {
    public ErrorBoundary? ComponentErrorBoundary { get; set; } = default;
}

What is an error handler?

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.


Why do you need a custom error handler?

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 and you also 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.


Build a custom error handler

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. We will create a sample custom error handler, in this sample, we will using a transfer service to store data. See Transfer Service for more information.

  1. Create a transfer service to persist the thrown exceptions. For example:
public class ExceptionRecorderService
{
    public ObservableCollection<Exception> Exceptions { get; set; } = new();
}
  1. Register the transfer service at Program.cs.
builder.Services.AddScoped<ExceptionRecorderService>();
  1. Create a new class that extends the ErrorBoundaryBase class. For example:
public class BlazorSchoolErrorBoundary : ErrorBoundaryBase
{
    public BlazorSchoolErrorBoundary()
    {
        MaximumErrorCount = 2;
    }
}
MaximumErrorCount is a property of the base class. You can change the value of MaximumErrorCount based on what you need.
  1. Inject the transfer service you have created at step 1. For example:
public class BlazorSchoolErrorBoundary : ErrorBoundaryBase
{
    ...
    [Inject]
    public ExceptionRecorderService ExceptionRecorderService { get; set; } = default!;
}
  1. Override the OnErrorAsync method to persist the exception to the transfer service. For example:
public class BlazorSchoolErrorBoundary : ErrorBoundaryBase
{
    ...
    protected override Task OnErrorAsync(Exception exception)
    {
        ExceptionRecorderService.Exceptions.Add(exception);

        return Task.CompletedTask;
    }
}
  1. Override the BuildRenderTree method to display an UI to the user when an error occurs. Provide a method to resolve error (optional). For example:
public class BlazorSchoolErrorBoundary : ErrorBoundaryBase
{
    ...
    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();
            }
        }
    }
}

Using custom error handler

After you have built your custom error handler, you can use it as with the ErrorHandler component. For example:

<div class="border border-primary border-5 p-3">
    <div>Custom error boundary with modified max error.</div>
    <BlazorSchoolErrorBoundary>
        <ChildContent>
            <TriggerError />
        </ChildContent>
        <ErrorContent>
            <TriggerError />
            <div>Throw more error and I will crash.</div>
        </ErrorContent>
    </BlazorSchoolErrorBoundary>
</div>

Tracking error

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.


Common mistakes

When handling an error, make sure you don't make the common mistake we are going to talk about in this section.

Throw error outside of error handler's scope

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://unknown-wesite-2234123.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.


Best practice

You have learnt how to handle errors, here are a few things you should keep in mind while developing website:

  • Use a 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.
  • In every layout you should have an error handler to trap all unhandled errors and act accordingly rather than always delegate to the global error handler and force the user to reload everytime there is an error.
  • Check for type of error in the error handler and act accordingly. For example, if there is an error from the API (HTTP error) you can use the status code to take necessary action. 401 Unauthorized error then you can redirect the user to the login page. 500 Internal Server Error then you can send the CurrentException object to your logging API.
  • For all other unhandled errors, send the error detail back to the logging API. You can use any third party error providers or build yourself one. You can then look at those logs and necessary changes to the website.
  • An ideal error report page should be a static html page with minimal functionality.
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-2025 Blazor School
An unhandled error has occurred. Reload 🗙