Skip to content

Handling errors in ComponentBase.InvokeAsync #27716

Closed
@poke

Description

@poke

Up until now, I was under the impression that I could use ComponentBase.InvokeAsync with server-side Blazor to lift some asynchronous work back onto the renderer. I use this primarily in cases where I have a C# event, which is synchronous, and I want to then do something asynchronously to update the component. For example:

@implements IDisposable
@inject SomeService someService

<ul>
  @if (isLoading)
  {
    <li>Loading…</li>
  }
  else
  {
    @foreach (var item in items)
    {
      <li>@item.Title</li>
    }
  }
</ul>

@code {
    bool isLoading = true;
    IList<Item> items;

    protected override void OnInitialized()
    {
        someService.ItemsChanged += HandleItemsChanged;
        isLoading = true;
    }

    public void Dispose()
    {
        someService.ItemsChanged -= HandleItemsChanged;
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            items = await someService.LoadItems();
            isLoading = false;
            StateHasChanged();
        }
    }

    HandleItemsChanged(object sender, EventArgs e)
    {
        InvokeAsync(async () =>
        {
            isLoading = true;
            StateHasChanged();

            items = await someService.LoadItems();
            isLoading = false;
            StateHasChanged();
        });
    }
}

This pattern has been working really well for me so far. I can load the data initially when the component is rendered, and when the underlying data is updated, the event causes the component to refresh its view.

Now, the LoadItems() is pretty safe and will rarely fail. In the rare case there is an exception, I am fine with the server-side Blazor application crashing. I have designed the error dialog for this purpose so users know that something critical happened and that they need to refresh the page to retry. This may not be the most graceful way to handle problems but it works pretty well in this case and does not require handling exceptions all over the place.

However, I now realized that exceptions that are thrown within the InvokeAsync action are not actually bubbling up to the renderer.

While the lifecycle methods are run directly through the renderer by calling AddToPendingTasks which will then handle errors in GetErrorHandledTask, it appears that call InvokeAsync will just run the action through the dispatcher which will eventually just return the task without doing any error handling.

So I am basically in an async void scenario without noticing it where exceptions thrown within InvokeAsync disappear into the void. – Ouch.

Is there any good approach on how to do global error handling for situations like this in server-side Blazor? I am not really looking forward to adding custom error handling to all my components. So if there was a way to trigger the default unhandled error experience, I would be very happy if I could just slap that around all InvokeAsync calls and keep the error behavior as I intended. Unfortunately, TryNotifyClientErrorAsync is not accessible from the outside and the other methods like DispatchEvent do more than I would like to do here.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions