Skip to content

Commit 5e64726

Browse files
authored
Fixed the ErrorBoundary multiple errors bug (#63254)
1 parent 56ecd59 commit 5e64726

File tree

6 files changed

+242
-6
lines changed

6 files changed

+242
-6
lines changed

src/Components/Components/src/ComponentBase.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,10 @@ private async Task RunInitAndSetParametersAsync()
283283
// to defer calling StateHasChanged up until the first bit of async code happens or until
284284
// the end. Additionally, we want to avoid calling StateHasChanged if no
285285
// async work is to be performed.
286-
StateHasChanged();
286+
if (task.Status != TaskStatus.Faulted)
287+
{
288+
StateHasChanged();
289+
}
287290

288291
try
289292
{
@@ -319,7 +322,10 @@ private Task CallOnParametersSetAsync()
319322

320323
// We always call StateHasChanged here as we want to trigger a rerender after OnParametersSet and
321324
// the synchronous part of OnParametersSetAsync has run.
322-
StateHasChanged();
325+
if (task.Status != TaskStatus.Faulted)
326+
{
327+
StateHasChanged();
328+
}
323329

324330
return shouldAwaitTask ?
325331
CallStateHasChangedOnAsyncCompletion(task) :

src/Components/Components/test/ComponentBaseTest.cs

Lines changed: 193 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,126 @@ public async Task DoesNotRenderAfterOnInitAsyncTaskIsCancelledUsingCancellationT
372372
Assert.NotEmpty(renderer.Batches);
373373
}
374374

375+
[Fact]
376+
public async Task ErrorBoundaryHandlesOnInitializedAsyncReturnFaultedTask()
377+
{
378+
// Arrange
379+
var renderer = new TestRenderer();
380+
TestErrorBoundary capturedBoundary = null;
381+
382+
// Create root component that wraps the TestComponentErrorBuildRenderTree in an TestErrorBoundary
383+
var rootComponent = new TestComponent();
384+
rootComponent.ChildContent = builder =>
385+
{
386+
builder.OpenComponent<TestErrorBoundary>(0);
387+
builder.AddComponentParameter(1, nameof(TestErrorBoundary.ChildContent), (RenderFragment)(childBuilder =>
388+
{
389+
childBuilder.OpenComponent<TestComponentErrorBuildRenderTree>(0);
390+
childBuilder.AddComponentParameter(1, nameof(TestComponentErrorBuildRenderTree.FaultedTaskOnInitializedAsync), true);
391+
childBuilder.CloseComponent();
392+
}));
393+
builder.AddComponentReferenceCapture(2, inst => capturedBoundary = (TestErrorBoundary)inst);
394+
builder.CloseComponent();
395+
};
396+
397+
// Act
398+
var rootComponentId = renderer.AssignRootComponentId(rootComponent);
399+
await renderer.RenderRootComponentAsync(rootComponentId);
400+
401+
// Assert
402+
Assert.NotNull(capturedBoundary);
403+
Assert.NotNull(capturedBoundary!.ReceivedException);
404+
Assert.Equal(typeof(InvalidTimeZoneException), capturedBoundary!.ReceivedException.GetType());
405+
}
406+
407+
[Fact]
408+
public async Task ErrorBoundaryHandlesCallOnParametersSetAsyncReturnFaultedTask()
409+
{
410+
// Arrange
411+
var renderer = new TestRenderer();
412+
TestErrorBoundary capturedBoundary = null;
413+
414+
// Create root component that wraps the TestComponentErrorBuildRenderTree in an TestErrorBoundary
415+
var rootComponent = new TestComponent();
416+
rootComponent.ChildContent = builder =>
417+
{
418+
builder.OpenComponent<TestErrorBoundary>(0);
419+
builder.AddComponentParameter(1, nameof(TestErrorBoundary.ChildContent), (RenderFragment)(childBuilder =>
420+
{
421+
childBuilder.OpenComponent<TestComponentErrorBuildRenderTree>(0);
422+
childBuilder.AddComponentParameter(1, nameof(TestComponentErrorBuildRenderTree.FaultedTaskOnParametersSetAsync), true);
423+
childBuilder.CloseComponent();
424+
}));
425+
builder.AddComponentReferenceCapture(2, inst => capturedBoundary = (TestErrorBoundary)inst);
426+
builder.CloseComponent();
427+
};
428+
429+
// Act
430+
var rootComponentId = renderer.AssignRootComponentId(rootComponent);
431+
await renderer.RenderRootComponentAsync(rootComponentId);
432+
433+
// Assert
434+
Assert.NotNull(capturedBoundary);
435+
Assert.NotNull(capturedBoundary!.ReceivedException);
436+
Assert.Equal(typeof(InvalidTimeZoneException), capturedBoundary!.ReceivedException.GetType());
437+
}
438+
439+
[Fact]
440+
public async Task ComponentBaseDoesntRenderWhenOnInitializedAsyncFaultedTask()
441+
{
442+
// Arrange
443+
var renderer = new TestRenderer();
444+
renderer.ShouldHandleExceptions = true;
445+
TestComponentErrorBuildRenderTree testComponentErrorBuildRenderTree = null;
446+
447+
// Create root component that wraps the TestComponentErrorBuildRenderTree in an TestErrorBoundary
448+
var rootComponent = new TestComponent();
449+
rootComponent.ChildContent = builder =>
450+
{
451+
builder.OpenComponent<TestComponentErrorBuildRenderTree>(0);
452+
builder.AddComponentParameter(1, nameof(TestComponentErrorBuildRenderTree.FaultedTaskOnInitializedAsync), true);
453+
builder.AddComponentReferenceCapture(2, inst => testComponentErrorBuildRenderTree = (TestComponentErrorBuildRenderTree)inst);
454+
builder.CloseComponent();
455+
};
456+
457+
// Act
458+
var rootComponentId = renderer.AssignRootComponentId(rootComponent);
459+
await renderer.RenderRootComponentAsync(rootComponentId);
460+
461+
// Assert
462+
Assert.IsType<InvalidTimeZoneException>(renderer.HandledExceptions[0]);
463+
Assert.NotNull(testComponentErrorBuildRenderTree);
464+
Assert.Equal(0, testComponentErrorBuildRenderTree.StateHasChangedCalled);
465+
}
466+
467+
[Fact]
468+
public async Task ComponentBaseDoesntRenderWhenOnSetParametersSetAsyncFaultedTask()
469+
{
470+
// Arrange
471+
var renderer = new TestRenderer();
472+
renderer.ShouldHandleExceptions = true;
473+
TestComponentErrorBuildRenderTree testComponentErrorBuildRenderTree = null;
474+
475+
// Create root component that wraps the TestComponentErrorBuildRenderTree in an TestErrorBoundary
476+
var rootComponent = new TestComponent();
477+
rootComponent.ChildContent = builder =>
478+
{
479+
builder.OpenComponent<TestComponentErrorBuildRenderTree>(0);
480+
builder.AddComponentParameter(1, nameof(TestComponentErrorBuildRenderTree.FaultedTaskOnParametersSetAsync), true);
481+
builder.AddComponentReferenceCapture(2, inst => testComponentErrorBuildRenderTree = (TestComponentErrorBuildRenderTree)inst);
482+
builder.CloseComponent();
483+
};
484+
485+
// Act
486+
var rootComponentId = renderer.AssignRootComponentId(rootComponent);
487+
await renderer.RenderRootComponentAsync(rootComponentId);
488+
489+
// Assert
490+
Assert.IsType<InvalidTimeZoneException>(renderer.HandledExceptions[0]);
491+
Assert.NotNull(testComponentErrorBuildRenderTree);
492+
Assert.Equal(0, testComponentErrorBuildRenderTree.StateHasChangedCalled);
493+
}
494+
375495
[Fact]
376496
public async Task DoesNotRenderAfterOnParametersSetAsyncTaskIsCanceled()
377497
{
@@ -491,11 +611,20 @@ private class TestComponent : ComponentBase
491611

492612
public int Counter { get; set; }
493613

614+
public RenderFragment ChildContent { get; set; }
615+
494616
protected override void BuildRenderTree(RenderTreeBuilder builder)
495617
{
496-
builder.OpenElement(0, "p");
497-
builder.AddContent(1, Counter);
498-
builder.CloseElement();
618+
if (ChildContent != null)
619+
{
620+
builder.AddContent(0, ChildContent);
621+
}
622+
else
623+
{
624+
builder.OpenElement(0, "p");
625+
builder.AddContent(1, Counter);
626+
builder.CloseElement();
627+
}
499628
}
500629

501630
protected override void OnInitialized()
@@ -570,4 +699,65 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
570699
}
571700
}
572701
}
702+
703+
private class TestErrorBoundary : ErrorBoundaryBase
704+
{
705+
public Exception ReceivedException => CurrentException;
706+
707+
protected override Task OnErrorAsync(Exception exception)
708+
{
709+
return Task.CompletedTask;
710+
}
711+
712+
protected override void BuildRenderTree(RenderTreeBuilder builder)
713+
{
714+
if (CurrentException == null)
715+
{
716+
builder.AddContent(0, ChildContent);
717+
}
718+
else
719+
{
720+
builder.OpenElement(2, "div");
721+
builder.AddAttribute(3, "class", "blazor-error-boundary");
722+
builder.CloseElement();
723+
}
724+
}
725+
}
726+
727+
private class TestComponentErrorBuildRenderTree : ComponentBase
728+
{
729+
[Parameter] public bool FaultedTaskOnInitializedAsync { get; set; } = false;
730+
[Parameter] public bool FaultedTaskOnParametersSetAsync { get; set; } = false;
731+
732+
public int StateHasChangedCalled { get; set; } = 0;
733+
734+
protected new void StateHasChanged()
735+
{
736+
StateHasChangedCalled++;
737+
base.StateHasChanged();
738+
}
739+
740+
protected override void BuildRenderTree(RenderTreeBuilder builder)
741+
{
742+
throw new InvalidOperationException("Error in BuildRenderTree");
743+
}
744+
745+
protected override Task OnInitializedAsync()
746+
{
747+
if (FaultedTaskOnInitializedAsync)
748+
{
749+
return Task.FromException(new InvalidTimeZoneException());
750+
}
751+
return Task.CompletedTask;
752+
}
753+
754+
protected override Task OnParametersSetAsync()
755+
{
756+
if (FaultedTaskOnParametersSetAsync)
757+
{
758+
return Task.FromException(new InvalidTimeZoneException());
759+
}
760+
return Task.CompletedTask;
761+
}
762+
}
573763
}

src/Components/WebAssembly/WebAssembly.Authentication/test/RemoteAuthenticatorCoreTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public async Task AuthenticationManager_Throws_ForInvalidAction()
3434
});
3535

3636
// Act & assert
37-
await Assert.ThrowsAsync<InvalidOperationException>(() => remoteAuthenticator.SetParametersAsync(parameters));
37+
await Assert.ThrowsAsync<NullReferenceException>(() => remoteAuthenticator.SetParametersAsync(parameters));
3838
}
3939

4040
[Fact]

src/Components/test/E2ETest/Tests/ErrorBoundaryTest.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,17 @@ public void CanHandleErrorsAfterDisposingComponent()
126126
AssertGlobalErrorState(false);
127127
}
128128

129+
[Fact]
130+
public void CanHandleErrorsAfterDisposingErrorBoundaryComponent()
131+
{
132+
var container = Browser.Exists(By.Id("multiple-errors-at-once-test"));
133+
container.FindElement(By.ClassName("throw-multiple-errors")).Click();
134+
// The error boundary is still there, so we see the error message
135+
Browser.Collection(() => container.FindElements(By.ClassName("error-message")),
136+
elem => Assert.Equal("OnInitializedAsyncError", elem.Text));
137+
AssertGlobalErrorState(false);
138+
}
139+
129140
[Fact]
130141
public async Task CanHandleErrorsAfterDisposingErrorBoundary()
131142
{

src/Components/test/testassets/BasicTestApp/ErrorBoundaryTest/ErrorBoundaryCases.razor

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,22 @@
7070
<button class="throw-in-errorcontent" @onclick="@(() => throwInErrorContent = true)">Throw in error content (causing infinite error loop)</button>
7171
</div>
7272

73+
<hr/>
74+
<h2>Two errors in child</h2>
75+
<div id="multiple-errors-at-once-test">
76+
@if (twoErrorsInChild) {
77+
<ErrorBoundary>
78+
<ChildContent>
79+
<MultipleErrorsChild />
80+
</ChildContent>
81+
<ErrorContent>
82+
<p class="error-message">@context.Message</p>
83+
</ErrorContent>
84+
</ErrorBoundary>
85+
}
86+
<button class="throw-multiple-errors" @onclick="@(() => twoErrorsInChild = true)">Throw multiple errors in child</button>
87+
</div>
88+
7389
<hr />
7490
<h2>Errors after disposal</h2>
7591
<p>Long-running tasks could fail after the component has been removed from the tree. We still want these failures to be captured by the error boundary they were inside when the task began, even if that error boundary itself has also since been disposed. Otherwise, error handling would behave differently based on whether the user has navigated away while processing was in flight, which would be very unexpected and hard to handle.</p>
@@ -122,6 +138,7 @@
122138
private bool disposalTestRemoveErrorBoundary;
123139
private bool disposalTestBeginDelayedError;
124140
private bool multipleChildrenBeginDelayedError;
141+
private bool twoErrorsInChild;
125142

126143
void EventHandlerErrorSync()
127144
=> throw new InvalidTimeZoneException("Synchronous error from event handler");
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<h3>MultipleErrorsChild</h3>
2+
3+
@{
4+
throw new Exception("BuildRenderTreeError");
5+
}
6+
7+
@code {
8+
protected override Task OnInitializedAsync()
9+
{
10+
return Task.FromException(new Exception("OnInitializedAsyncError"));
11+
}
12+
}

0 commit comments

Comments
 (0)