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:
You can download the example code used in this topic on GitHub.
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.
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.
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 categories of errors:
You can catch an error in 5 scopes:
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 |
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.
Program.cs
. For example:app.UseExceptionHandler("/BlazorSchoolError");
@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>
@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}"); } } }
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.
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>
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>
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>();
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; } }
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:
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"> 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; }
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 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.
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.
public class ExceptionRecorderService { public ObservableCollection<Exception> Exceptions { get; set; } = new(); }
builder.Services.AddScoped<ExceptionRecorderService>();
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 ofMaximumErrorCount
based on what you need.
public class BlazorSchoolErrorBoundary : ErrorBoundaryBase { ... [Inject] public ExceptionRecorderService ExceptionRecorderService { get; set; } = default!; }
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; } }
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(); } } } }
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>
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://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.
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.