Skip to content

Commit 7111c8f

Browse files
Handle async exceptions when not rendering root component. Fixes #8151
1 parent 7b24304 commit 7111c8f

File tree

2 files changed

+49
-8
lines changed

2 files changed

+49
-8
lines changed

src/Components/Components/src/Rendering/Renderer.cs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -328,13 +328,14 @@ internal void AddToPendingTasks(Task task)
328328
HandleException(task.Exception.GetBaseException());
329329
break;
330330
default:
331-
// We are not in rendering the root component.
332-
if (_pendingTasks == null)
333-
{
334-
return;
335-
}
336-
337-
_pendingTasks.Add(GetErrorHandledTask(task));
331+
// It's important to evaluate the following even if we're not going to use
332+
// handledErrorTask below, because it has the side-effect of calling HandleException.
333+
var handledErrorTask = GetErrorHandledTask(task);
334+
335+
// The pendingTasks collection is only used during prerendering to track quiescence,
336+
// so will be null at other times.
337+
_pendingTasks?.Add(handledErrorTask);
338+
338339
break;
339340
}
340341
}

src/Components/Components/test/RendererTest.cs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2683,7 +2683,7 @@ public void ExceptionsReturnedUsingTaskFromExceptionCanBeHandled()
26832683
}
26842684

26852685
[Fact]
2686-
public async Task ExceptionsThrownAsynchronouslyCanBeHandled()
2686+
public async Task ExceptionsThrownAsynchronouslyDuringFirstRenderCanBeHandled()
26872687
{
26882688
// Arrange
26892689
var renderer = new TestRenderer { ShouldHandleExceptions = true };
@@ -2722,6 +2722,36 @@ public async Task ExceptionsThrownAsynchronouslyCanBeHandled()
27222722
Assert.Same(exception, Assert.Single(renderer.HandledExceptions).GetBaseException());
27232723
}
27242724

2725+
[Fact]
2726+
public async Task ExceptionsThrownAsynchronouslyAfterFirstRenderCanBeHandled()
2727+
{
2728+
// This differs from the "during first render" case, because some aspects of the rendering
2729+
// code paths are special cased for the first render because of prerendering.
2730+
2731+
// Arrange
2732+
var renderer = new TestRenderer { ShouldHandleExceptions = true };
2733+
var taskToAwait = Task.CompletedTask;
2734+
var component = new TestComponent(builder =>
2735+
{
2736+
builder.OpenComponent<ComponentThatAwaitsTask>(0);
2737+
builder.AddAttribute(1, nameof(ComponentThatAwaitsTask.TaskToAwait), taskToAwait);
2738+
builder.CloseComponent();
2739+
});
2740+
var componentId = renderer.AssignRootComponentId(component);
2741+
await renderer.RenderRootComponentAsync(componentId); // Not throwing on first render
2742+
2743+
var asyncExceptionTcs = new TaskCompletionSource<object>();
2744+
taskToAwait = asyncExceptionTcs.Task;
2745+
await renderer.Invoke(component.TriggerRender);
2746+
2747+
// Act
2748+
var exception = new InvalidOperationException();
2749+
asyncExceptionTcs.SetException(exception);
2750+
2751+
// Assert
2752+
Assert.Same(exception, Assert.Single(renderer.HandledExceptions).GetBaseException());
2753+
}
2754+
27252755
[Fact]
27262756
public async Task ExceptionsThrownAsynchronouslyFromMultipleComponentsCanBeHandled()
27272757
{
@@ -3696,5 +3726,15 @@ public enum EventType
36963726
OnAfterRenderAsync,
36973727
}
36983728
}
3729+
3730+
private class ComponentThatAwaitsTask : ComponentBase
3731+
{
3732+
[Parameter] public Task TaskToAwait { get; set; }
3733+
3734+
protected override async Task OnParametersSetAsync()
3735+
{
3736+
await TaskToAwait;
3737+
}
3738+
}
36993739
}
37003740
}

0 commit comments

Comments
 (0)