diff --git a/src/Components/Components/src/Rendering/Renderer.cs b/src/Components/Components/src/Rendering/Renderer.cs index bf64cdf4a3..44904eb398 100644 --- a/src/Components/Components/src/Rendering/Renderer.cs +++ b/src/Components/Components/src/Rendering/Renderer.cs @@ -328,13 +328,14 @@ namespace Microsoft.AspNetCore.Components.Rendering HandleException(task.Exception.GetBaseException()); break; default: - // We are not in rendering the root component. - if (_pendingTasks == null) - { - return; - } + // It's important to evaluate the following even if we're not going to use + // handledErrorTask below, because it has the side-effect of calling HandleException. + var handledErrorTask = GetErrorHandledTask(task); - _pendingTasks.Add(GetErrorHandledTask(task)); + // The pendingTasks collection is only used during prerendering to track quiescence, + // so will be null at other times. + _pendingTasks?.Add(handledErrorTask); + break; } } diff --git a/src/Components/Components/test/RendererTest.cs b/src/Components/Components/test/RendererTest.cs index 8d67ab50c9..df713ec9d5 100644 --- a/src/Components/Components/test/RendererTest.cs +++ b/src/Components/Components/test/RendererTest.cs @@ -2683,7 +2683,7 @@ namespace Microsoft.AspNetCore.Components.Test } [Fact] - public async Task ExceptionsThrownAsynchronouslyCanBeHandled() + public async Task ExceptionsThrownAsynchronouslyDuringFirstRenderCanBeHandled() { // Arrange var renderer = new TestRenderer { ShouldHandleExceptions = true }; @@ -2722,6 +2722,36 @@ namespace Microsoft.AspNetCore.Components.Test Assert.Same(exception, Assert.Single(renderer.HandledExceptions).GetBaseException()); } + [Fact] + public async Task ExceptionsThrownAsynchronouslyAfterFirstRenderCanBeHandled() + { + // This differs from the "during first render" case, because some aspects of the rendering + // code paths are special cased for the first render because of prerendering. + + // Arrange + var renderer = new TestRenderer { ShouldHandleExceptions = true }; + var taskToAwait = Task.CompletedTask; + var component = new TestComponent(builder => + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(ComponentThatAwaitsTask.TaskToAwait), taskToAwait); + builder.CloseComponent(); + }); + var componentId = renderer.AssignRootComponentId(component); + await renderer.RenderRootComponentAsync(componentId); // Not throwing on first render + + var asyncExceptionTcs = new TaskCompletionSource(); + taskToAwait = asyncExceptionTcs.Task; + await renderer.Invoke(component.TriggerRender); + + // Act + var exception = new InvalidOperationException(); + asyncExceptionTcs.SetException(exception); + + // Assert + Assert.Same(exception, Assert.Single(renderer.HandledExceptions).GetBaseException()); + } + [Fact] public async Task ExceptionsThrownAsynchronouslyFromMultipleComponentsCanBeHandled() { @@ -3696,5 +3726,15 @@ namespace Microsoft.AspNetCore.Components.Test OnAfterRenderAsync, } } + + private class ComponentThatAwaitsTask : ComponentBase + { + [Parameter] public Task TaskToAwait { get; set; } + + protected override async Task OnParametersSetAsync() + { + await TaskToAwait; + } + } } }