Handle errors in ASP.NET Core Blazor apps
This article describes how Blazor manages unhandled exceptions and how to develop apps that detect and handle errors.
Detailed errors during development
When a Blazor app isn't functioning properly during development, receiving detailed error information from the app assists in troubleshooting and fixing the issue. When an error occurs, Blazor apps display a gold bar at the bottom of the screen:
- During development, the gold bar directs you to the browser console, where you can see the exception.
- In production, the gold bar notifies the user that an error has occurred and recommends refreshing the browser.
The UI for this error handling experience is part of the Blazor project templates.
In a Blazor WebAssembly app, customize the experience in the wwwroot/index.html
file:
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
In a Blazor Server app, customize the experience in the Pages/_Host.cshtml
file:
<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>
The blazor-error-ui
element is hidden by the styles included in the Blazor templates (wwwroot/css/app.css
or wwwroot/css/site.css
) and then shown when an error occurs:
#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
Blazor Server detailed circuit errors
Client-side errors don't include the callstack and don't provide detail on the cause of the error, but server logs do contain such information. For development purposes, sensitive circuit error information can be made available to the client by enabling detailed errors.
Enable Blazor Server detailed errors using the following approaches:
- CircuitOptions.DetailedErrors.
- The
DetailedErrors
configuration key set totrue
, which can be set in the app's Development settings file (appsettings.Development.json
). The key can also be set using theASPNETCORE_DETAILEDERRORS
environment variable with a value oftrue
. - SignalR server-side logging (
Microsoft.AspNetCore.SignalR
) can be set to Debug or Trace for detailed SignalR logging.
appsettings.Development.json
:
{
"DetailedErrors": true,
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.AspNetCore.SignalR": "Debug"
}
}
}
Warning
Exposing error information to clients on the Internet is a security risk that should always be avoided.
How a Blazor Server app reacts to unhandled exceptions
Blazor Server is a stateful framework. While users interact with an app, they maintain a connection to the server known as a circuit. The circuit holds active component instances, plus many other aspects of state, such as:
- The most recent rendered output of components.
- The current set of event-handling delegates that could be triggered by client-side events.
If a user opens the app in multiple browser tabs, they have multiple independent circuits.
Blazor treats most unhandled exceptions as fatal to the circuit where they occur. If a circuit is terminated due to an unhandled exception, the user can only continue to interact with the app by reloading the page to create a new circuit. Circuits outside of the one that's terminated, which are circuits for other users or other browser tabs, aren't affected. This scenario is similar to a desktop app that crashes. The crashed app must be restarted, but other apps aren't affected.
A circuit is terminated when an unhandled exception occurs for the following reasons:
- An unhandled exception often leaves the circuit in an undefined state.
- The app's normal operation can't be guaranteed after an unhandled exception.
- Security vulnerabilities may appear in the app if the circuit continues.
Manage unhandled exceptions in developer code
For an app to continue after an error, the app must have error handling logic. Later sections of this article describe potential sources of unhandled exceptions.
In production, don't render framework exception messages or stack traces in the UI. Rendering exception messages or stack traces could:
- Disclose sensitive information to end users.
- Help a malicious user discover weaknesses in an app that can compromise the security of the app, server, or network.
Log errors with a persistent provider
If an unhandled exception occurs, the exception is logged to ILogger instances configured in the service container. By default, Blazor apps log to console output with the Console Logging Provider. Consider logging to a more permanent location with a provider that manages log size and log rotation. For more information, see Logging in .NET Core and ASP.NET Core.
During development, Blazor usually sends the full details of exceptions to the browser's console to aid in debugging. In production, detailed errors in the browser's console are disabled by default, which means that errors aren't sent to clients but the exception's full details are still logged server-side. For more information, see Handle errors in ASP.NET Core.
You must decide which incidents to log and the level of severity of logged incidents. Hostile users might be able to trigger errors deliberately. For example, don't log an incident from an error where an unknown ProductId
is supplied in the URL of a component that displays product details. Not all errors should be treated as high-severity incidents for logging.
For more information, see ASP.NET Core Blazor logging.
Places where errors may occur
Framework and app code may trigger unhandled exceptions in any of the following locations:
- Component instantiation
- Lifecycle methods
- Rendering logic
- Event handlers
- Component disposal
- JavaScript interop
- Blazor Server rerendering
The preceding unhandled exceptions are described in the following sections of this article.
Component instantiation
When Blazor creates an instance of a component:
- The component's constructor is invoked.
- The constructors of any non-singleton DI services supplied to the component's constructor via the
@inject
directive or the[Inject]
attribute are invoked.
A Blazor Server circuit fails when any executed constructor or a setter for any [Inject]
property throws an unhandled exception. The exception is fatal because the framework can't instantiate the component. If constructor logic may throw exceptions, the app should trap the exceptions using a try-catch
statement with error handling and logging.
Lifecycle methods
During the lifetime of a component, Blazor invokes the following lifecycle methods:
- OnInitialized / OnInitializedAsync
- OnParametersSet / OnParametersSetAsync
- ShouldRender
- OnAfterRender / OnAfterRenderAsync
If any lifecycle method throws an exception, synchronously or asynchronously, the exception is fatal to a Blazor Server circuit. For components to deal with errors in lifecycle methods, add error handling logic.
In the following example where OnParametersSetAsync calls a method to obtain a product:
- An exception thrown in the
ProductRepository.GetProductByIdAsync
method is handled by atry-catch
statement. - When the
catch
block is executed:loadFailed
is set totrue
, which is used to display an error message to the user.- The error is logged.
@page "/product-details/{ProductId:int}"
@using Microsoft.Extensions.Logging
@inject IProductRepository ProductRepository
@inject ILogger<ProductDetails> Logger
@if (details != null)
{
<h1>@details.ProductName</h1>
<p>@details.Description</p>
}
else if (loadFailed)
{
<h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
<h1>Loading...</h1>
}
@code {
private ProductDetail details;
private bool loadFailed;
[Parameter]
public int ProductId { get; set; }
protected override async Task OnParametersSetAsync()
{
try
{
loadFailed = false;
details = await ProductRepository.GetProductByIdAsync(ProductId);
}
catch (Exception ex)
{
loadFailed = true;
Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
}
}
}
@page "/product-details/{ProductId:int}"
@using Microsoft.Extensions.Logging
@inject IProductRepository ProductRepository
@inject ILogger<ProductDetails> Logger
@if (details != null)
{
<h1>@details.ProductName</h1>
<p>@details.Description</p>
}
else if (loadFailed)
{
<h1>Sorry, we could not load this product due to an error.</h1>
}
else
{
<h1>Loading...</h1>
}
@code {
private ProductDetail details;
private bool loadFailed;
[Parameter]
public int ProductId { get; set; }
protected override async Task OnParametersSetAsync()
{
try
{
loadFailed = false;
details = await ProductRepository.GetProductByIdAsync(ProductId);
}
catch (Exception ex)
{
loadFailed = true;
Logger.LogWarning(ex, "Failed to load product {ProductId}", ProductId);
}
}
}
Rendering logic
The declarative markup in a .razor
component file is compiled into a C# method called BuildRenderTree. When a component renders, BuildRenderTree executes and builds up a data structure describing the elements, text, and child components of the rendered component.
Rendering logic can throw an exception. An example of this scenario occurs when @someObject.PropertyName
is evaluated but @someObject
is null
. An unhandled exception thrown by rendering logic is fatal to a Blazor Server circuit.
To prevent a null reference exception in rendering logic, check for a null
object before accessing its members. In the following example, person.Address
properties aren't accessed if person.Address
is null
:
@if (person.Address != null)
{
<div>@person.Address.Line1</div>
<div>@person.Address.Line2</div>
<div>@person.Address.City</div>
<div>@person.Address.Country</div>
}
@if (person.Address != null)
{
<div>@person.Address.Line1</div>
<div>@person.Address.Line2</div>
<div>@person.Address.City</div>
<div>@person.Address.Country</div>
}
The preceding code assumes that person
isn't null
. Often, the structure of the code guarantees that an object exists at the time the component is rendered. In those cases, it isn't necessary to check for null
in rendering logic. In the prior example, person
might be guaranteed to exist because person
is created when the component is instantiated.
Event handlers
Client-side code triggers invocations of C# code when event handlers are created using:
@onclick
@onchange
- Other
@on...
attributes @bind
Event handler code might throw an unhandled exception in these scenarios.
If an event handler throws an unhandled exception (for example, a database query fails), the exception is fatal to a Blazor Server circuit. If the app calls code that could fail for external reasons, trap exceptions using a try-catch
statement with error handling and logging.
If user code doesn't trap and handle the exception, the framework logs the exception and terminates the circuit.
Component disposal
A component may be removed from the UI, for example, because the user has navigated to another page. When a component that implements System.IDisposable is removed from the UI, the framework calls the component's Dispose method.
If the component's Dispose
method throws an unhandled exception, the exception is fatal to a Blazor Server circuit. If disposal logic may throw exceptions, the app should trap the exceptions using a try-catch
statement with error handling and logging.
For more information on component disposal, see ASP.NET Core Blazor lifecycle.
JavaScript interop
IJSRuntime.InvokeAsync allows .NET code to make asynchronous calls to the JavaScript runtime in the user's browser.
The following conditions apply to error handling with InvokeAsync:
- If a call to InvokeAsync fails synchronously, a .NET exception occurs. A call to InvokeAsync may fail, for example, because the supplied arguments can't be serialized. Developer code must catch the exception. If app code in an event handler or component lifecycle method doesn't handle an exception, the resulting exception is fatal to a Blazor Server circuit.
- If a call to InvokeAsync fails asynchronously, the .NET Task fails. A call to InvokeAsync may fail, for example, because the JavaScript-side code throws an exception or returns a
Promise
that completed asrejected
. Developer code must catch the exception. If using theawait
operator, consider wrapping the method call in atry-catch
statement with error handling and logging. Otherwise, the failing code results in an unhandled exception that's fatal to a Blazor Server circuit. - By default, calls to InvokeAsync must complete within a certain period or else the call times out. The default timeout period is one minute. The timeout protects the code against a loss in network connectivity or JavaScript code that never sends back a completion message. If the call times out, the resulting System.Threading.Tasks fails with an OperationCanceledException. Trap and process the exception with logging.
Similarly, JavaScript code may initiate calls to .NET methods indicated by the [JSInvokable]
attribute. If these .NET methods throw an unhandled exception:
- The exception isn't treated as fatal to a Blazor Server circuit.
- The JavaScript-side
Promise
is rejected.
You have the option of using error handling code on either the .NET side or the JavaScript side of the method call.
For more information, see the following articles:
- Call JavaScript functions from .NET methods in ASP.NET Core Blazor
- Call .NET methods from JavaScript functions in ASP.NET Core Blazor
Blazor Server prerendering
Blazor components can be prerendered using the Component Tag Helper so that their rendered HTML markup is returned as part of the user's initial HTTP request. This works by:
- Creating a new circuit for all of the prerendered components that are part of the same page.
- Generating the initial HTML.
- Treating the circuit as
disconnected
until the user's browser establishes a SignalR connection back to the same server. When the connection is established, interactivity on the circuit is resumed and the components' HTML markup is updated.
If any component throws an unhandled exception during prerendering, for example, during a lifecycle method or in rendering logic:
- The exception is fatal to the circuit.
- The exception is thrown up the call stack from the ComponentTagHelper Tag Helper. Therefore, the entire HTTP request fails unless the exception is explicitly caught by developer code.
Under normal circumstances when prerendering fails, continuing to build and render the component doesn't make sense because a working component can't be rendered.
To tolerate errors that may occur during prerendering, error handling logic must be placed inside a component that may throw exceptions. Use try-catch
statements with error handling and logging. Instead of wrapping the ComponentTagHelper Tag Helper in a try-catch
statement, place error handling logic in the component rendered by the ComponentTagHelper Tag Helper.
Advanced scenarios
Recursive rendering
Components can be nested recursively. This is useful for representing recursive data structures. For example, a TreeNode
component can render more TreeNode
components for each of the node's children.
When rendering recursively, avoid coding patterns that result in infinite recursion:
- Don't recursively render a data structure that contains a cycle. For example, don't render a tree node whose children includes itself.
- Don't create a chain of layouts that contain a cycle. For example, don't create a layout whose layout is itself.
- Don't allow an end user to violate recursion invariants (rules) through malicious data entry or JavaScript interop calls.
Infinite loops during rendering:
- Causes the rendering process to continue forever.
- Is equivalent to creating an unterminated loop.
In these scenarios, an affected Blazor Server circuit fails, and the thread usually attempts to:
- Consume as much CPU time as permitted by the operating system, indefinitely.
- Consume an unlimited amount of server memory. Consuming unlimited memory is equivalent to the scenario where an unterminated loop adds entries to a collection on every iteration.
To avoid infinite recursion patterns, ensure that recursive rendering code contains suitable stopping conditions.
Custom render tree logic
Most Blazor components are implemented as .razor
files and are compiled to produce logic that operates on a RenderTreeBuilder to render their output. A developer may manually implement RenderTreeBuilder logic using procedural C# code. For more information, see ASP.NET Core Blazor advanced scenarios.
Warning
Use of manual render tree builder logic is considered an advanced and unsafe scenario, not recommended for general component development.
If RenderTreeBuilder code is written, the developer must guarantee the correctness of the code. For example, the developer must ensure that:
- Calls to OpenElement and CloseElement are correctly balanced.
- Attributes are only added in the correct places.
Incorrect manual render tree builder logic can cause arbitrary undefined behavior, including crashes, server hangs, and security vulnerabilities.
Consider manual render tree builder logic on the same level of complexity and with the same level of danger as writing assembly code or MSIL instructions by hand.