Skip to content

Instantly share code, notes, and snippets.

@SteveSandersonMS
Last active April 10, 2024 13:26
Show Gist options
  • Save SteveSandersonMS/9451f3b5497ce2b5ad16b0d07ad73539 to your computer and use it in GitHub Desktop.
Save SteveSandersonMS/9451f3b5497ce2b5ad16b0d07ad73539 to your computer and use it in GitHub Desktop.
Error handling in Server-Side Blazor

Error handling in Server-Side Blazor

Developers building Blazor applications should be aware of how the framework deals with exceptions, and what steps to take in order to maximize reliability and to detect and diagnose errors.

To recap, server-side Blazor is a stateful framework. For as long as users are interacting with your application, they maintain a connection to the server known as a circuit. The circuit holds all the active component instances, plus many other aspects of state such as the components' most recent render output and the current set of event-handling delegates that could be triggered by client-side events. If a user opens your application in multiple browser tabs, then they have multiple independent circuits.

As a high-level principle, Blazor treats most unhandled exceptions as fatal to that circuit. If a circuit is terminated due to an unhandled exception, the user can only continue by reloading the page to create a new circuit and starting again, although other circuits (e.g., those for other users, or other browser tabs) would not be affected. This is very similar to a desktop application crashing: that application must be restarted, but other applications would not be affected.

The reason for treating most unhandled exceptions as fatal to the circuit is that they could leave the circuit in an undefined state. If the framework attempted to continue regardless, the application's normal operations could not be guaranteed and it may even lead to security issues.

So, if you want your users to be able to continue after errors occur (for example, when loading or saving data), you need to add suitable error handling logic (for example, using try/catch). This document describes the places where you should consider doing that.

Logging errors

If an unhandled exception does occur, it will be logged to the ILogger configured in your dependency injection system. By default this is only logged to the console output, so you may wish to consider directing the logs to a more permanent location. For more information about this, see documentation about logging in ASP.NET Core.

During development, Blazor will send full details of exceptions to your browser console to aid debugging when possible. In production, detailed errors are disabled by default, which means they are not sent to clients but their full details will still be logged. For more information, see the general error handling documentation for ASP.NET Core.

Places where errors may occur

Depending on your application logic, your code may trigger unhandled exceptions in any of the following places.

Component instantiation

When Blazor creates instances of your components, it invokes their constructors, as well as constructors for any DI services being supplied to them via @inject or the [Inject] attribute. If any of these constructors throws an exception, or if the setters for [Inject] properties throw exceptions, this is fatal to the circuit because it's impossible for the framework to carry out the intentions of the developer.

Lifecycle methods

During the lifetime of components, Blazor invokes lifecycle methods on components such as OnInitialized, OnParametersSet, ShouldRender, OnAfterRender, and the ...Async versions of these. If any of these lifecycle methods throws an exception, synchronously or asynchronously, this is fatal to the circuit because the framework no longer knows whether or how to render that component.

If you want your components to deal with errors, be sure to add suitable error handling logic. For example, in a component file called ProductDetails.razor,

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

@functions {
    [Parameter] public int ProductId { get; set; }

    ProductDetails details;
    bool loadFailed;

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

It's up to you to decide whether to log such incidents, and with what level of severity. Bear in mind that hostile users might be able to trigger these errors deliberately (e.g., by supplying an unknown ProductId in the URL), so you should not necessarily treat them all as high-severity incidents.

Be careful not to disclose sensitive information to end users. For example, you should not normally render exception messages or stack traces in the UI when your application is running in production, as doing so may help a malicious user discover how better to harm you.

Rendering logic

The declarative markup in a .razor file is compiled into a C# method called BuildRenderTree. Whenever your component renders, that method executes and builds up a data structure describing the elements, text, and child components being output by your component.

It's possible for your rendering logic to throw exceptions, for example if you try to evaluate an expression such as @someObject.PropertyName when @someObject is null. Unhandled exceptions thrown by rendering logic are fatal to the circuit, so be careful not to let this happen in production. This is just the same as in any other C# code. For example, to avoid a null reference exception you could use logic similar to the following:

@if (person.Address != null)
{
    <div>@person.Address.Line1</div>
    <div>@person.Address.Line2</div>
    <div>@person.Address.City</div>
    <div>@person.Address.Country</div>
}

This code still assumes that @person will not be null. Often you will know whether or not certain data will necessarily already be populated at the time you are rendering it.

Event handlers

When you set up event handlers using @onclick, @onchange, etc., or when you use @bind, you create the ability for client-side code to trigger invocations of your C# code. Your event handler code might throw an unhandled exception.

If an event handler throws an unhandled exception, it is fatal to the circuit because the framework cannot carry out the developer's intentions. As such, if you know you are calling code that could fail for external reasons (e.g., database queries) then you need to add suitable try/catch logic and whatever error handling and logging you want. Otherwise by default, the framework will log the exception and terminate the circuit.

Component disposal

When a component that implements System.IDisposable is removed from the UI, for example because the user has navigated to another page, the framework will call its Dispose method.

If the component's Dispose method throws an unhandled exception, this is treated as fatal to the circuit. If you know that your disposal logic may throw exceptions, then be sure to add suitable try/catch logic and whatever error handling and logging you want.

JavaScript interop

Blazor supplies a DI service, IJSRuntime, with a method InvokeAsync<T> that allows .NET code to make asynchronous calls to the JavaScript runtime in the user's browser. For more details, see documentation about JavaScript interop. Here are some details about error handling in InvokeAsync<T>:

  • If a call to InvokeAsync<T> fails synchronously, for example because the supplied arguments cannot be serialized, this results in a .NET exception. It's up to your code to catch that exception if you wish to handle it. If you don't handle it, it will result in an unhandled exception on whichever event handler or component lifecycle method made the call, which may be fatal to the circuit.

  • If a call to InvokeAsync<T> fails asynchronously, for example because the JavaScript-side code threw an exception or returned a promise that completed as rejected, this results in the .NET Task failing. It's up to your code to handle this however you wish: if you're using await, then you should consider wrapping it in try/catch logic otherwise it will result in an unhandled exception.

  • By default, calls to InvokeAsync<T> must complete within a certain period or will time out. The default timeout period is one minute. This is to protect your code against network connectivity loss, or misbehaving JavaScript code that might choose never to send back a completion message. If the call times out, the resulting Task will fail with an OperationCanceledException.

Similarly, it's possible for JavaScript code to initiate calls to .NET methods indicated by the [JSInvokable] attribute. If those .NET methods throw an unhandled exception, it is not treated as fatal to the circuit, but instead simply causes the JavaScript-side Promise to be rejected. This gives you the option to put error handling code on either the .NET side or the JavaScript side.

Circuit handlers

Blazor allows your code to define a circuit handler, which receives notifications when the state of a user's circuit changes between initialized, connected, disconnected, and disposed. This is achieved by registering a DI service that inherits from the CircuitHandler abstract base class.

If a custom circuit handler's methods throw an unhandled exception, this is treated as fatal to the circuit, so if you need to tolerate failures in whatever code you are invoking, you should use try/catch and implement whatever error handling and logging you want.

Circuit disposal

When a circuit finally ends because a user has disconnected and the circuit state is being cleaned up be the framework, the framework will dispose the circuit's DI scope, which in turn will dispose any circuit-scoped DI services that implement IDisposable. If any of the DI services throws an unhandled exception during disposal, this will be logged.

Prerendering

You can prerender Blazor components using the API Html.RenderComponentAsync so that their rendered HTML markup is returned as part of the user's initial HTTP request. This works by creating a new circuit containing all the components being prerendered as part of the same page, generating the initial HTML, then treating that circuit as "disconnected" until the user's browser establishes a SignalR connection back to the same server to resume interactivity on that circuit.

If any of your components throw an unhandled exception while being prerendered (e.g., during their lifecycle methods or rendering logic), this is treated as fatal to the circuit. Additionally, the exception will be thrown up the call stack from the Html.RenderComponentAsync call, so the entire HTTP request will fail unless you specificially catch this exception.

It doesn't normally make sense to try to continue if prerendering fails, since you cannot produce any working UI for the user. If you need to tolerate certain errors that may occur during prerendering, you must place error handling logic inside the components that may throw these exceptions (for example, use try/catch and implement suitable logic to log errors and display an error state to the user).

Caveats for advanced scenarios

Recursive rendering

Components may be nested recursively. This is useful, for example, to represent recursive data structures such as trees - you might have a <TreeNode> component that also renders more <TreeNode> components for each of the node's children.

However, you must take care not to create infinite recursion. This would happen if you recursively render a data structure that contains a cycle (e.g., a tree node whose children includes itself), or if you have a chain of layouts that contains a cycle (e.g., a layout whose layout is itself).

Infinite loops during rendering will cause the rendering process to continue forever. It's equivalent to having an unterminated loop. The affected circuit will hang, and the thread will consume as much CPU time as possible, indefinitely. It may also consume an unlimited amount of server memory, equivalent to the scenario where an unterminated loop adds entries to a collection on every iteration.

Be careful not to create this situation. To avoid it, ensure that any recursive rendering contains suitable stopping conditions or enforces invariants, for example that a tree structure does not contain cycles. Do not allow an end user to violate invariants such as these through malicious data entry or JavaScript interop calls.

Custom render tree logic

Most Blazor components are implemented as .razor files, which are then compiled to produce logic that operates on a RenderTreeBuilder to render their output. However it's also possible to implement RenderTreeBuilder logic manually using procedural C# code.

Manual render tree builder logic is considered an advanced and unsafe scenario. If you choose to write such low-level code, the responsibility is on you to guarantee the correctness of your code. For example, you must ensure that calls to OpenElement and CloseElement are correctly balanced, and that attributes are added only in the correct places. Incorrect manual render tree builder logic can cause arbitrary undefined behavior, including crashes, server hangs, or security issues. You should think of it as being like writing assembly code or MSIL instructions by hand.

@Mike-E-angelo
Copy link

FWIW started a thread on reddit around this topic here, and added my own take/solution:

https://www.reddit.com/r/Blazor/comments/fcx27p/exception_handling_strategies/

@bvirkler
Copy link

bvirkler commented Mar 7, 2020

Any plans to implement global error handling?

See dotnet/aspnetcore#13452

@yandoldonov
Copy link

hi everyone, any progress on this since this has been touched on last? any sign of global error handling that might have come around? thanks!

@michalkrzych
Copy link

hi everyone, any progress on this since this has been touched on last? any sign of global error handling that might have come around? thanks!

@yandoldonov I believe the <ErrorBoundary> is your friend https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/handle-errors?view=aspnetcore-7.0

@yandoldonov
Copy link

@michalkrzych thanks for this! yes, this seems to be the path, been using them for a while now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment