Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/Components/Components/src/ComponentBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,15 @@ public ComponentBase()
};
}

/// <summary>
/// Gets or sets a value that indicates whether there is a pending queued render.
/// </summary>
internal bool HasPendingQueuedRender
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way how we could implemement this fix for ErrorBoundary components without reducing encapsulation for every component? E.g. by overriding StateHasChanged for ErrorBoundary.

{
get => _hasPendingQueuedRender;
set => _hasPendingQueuedRender = value;
}

/// <summary>
/// Gets the <see cref="Components.RendererInfo"/> the component is running on.
/// </summary>
Expand Down
7 changes: 7 additions & 0 deletions src/Components/Components/src/RenderTree/Renderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1176,6 +1176,13 @@ private void HandleExceptionViaErrorBoundary(Exception error, ComponentState? er
// making it render an empty fragment. Ensures that failed components don't continue to
// operate, which would be a whole new kind of edge case to support forever.
AddToRenderQueue(candidate.ComponentId, builder => { });
if (candidate.Component is ComponentBase componentBase)
{
// If the ErrorBoundary already handled one error and caught another, there can be a bug, where
// HasPendingQueuedRender is still true from the previous error handling render. Clear it to avoid
// rendering empty fragment and skipping the ErrorContent.
componentBase.HasPendingQueuedRender = false;
}

try
{
Expand Down
10 changes: 10 additions & 0 deletions src/Components/test/E2ETest/Tests/ErrorBoundaryTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,16 @@ public void CanHandleErrorsInlineInErrorBoundaryContent()
AssertGlobalErrorState(true);
}

[Fact]
public void CanHandleMultipleExceptionsForOnce()
{
var container = Browser.Exists(By.Id("throw-multiple-errors-foreach-test"));
container.FindElement(By.ClassName("throw-multiple-errors-foreach")).Click();
Browser.Collection(() => container.FindElements(By.ClassName("error-message")),
elem => Assert.Equal("There was an error.", elem.Text));
AssertGlobalErrorState(false);
}

[Fact]
public void CanHandleErrorsAfterDisposingComponent()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,26 @@
</ErrorIgnorer>
</div>

<hr />
<h2>Error boundary can handle multiple errors thrown</h2>
<div id="throw-multiple-errors-foreach-test">
<ErrorBoundary>
<ChildContent>
@if (throwMultipleErrorsForeach)
{
@foreach (var item in Enumerable.Range(1, 3))
{
<ForeachErrorsChild />
}
}
</ChildContent>
<ErrorContent>
<p class="error-message">There was an error.</p>
</ErrorContent>
</ErrorBoundary>
<button class="throw-multiple-errors-foreach" @onclick="@(() => throwMultipleErrorsForeach = true)">Throw multiple errors from child</button>
</div>

<hr />
<h2>Exception inline in error boundary markup</h2>
<p>This shows that, if an ErrorBoundary itself fails while rendering its own ChildContent, then it can catch its own exception. But if the error comes from the error content, this triggers the "infinite error loop" detection logic and becomes fatal.</p>
Expand Down Expand Up @@ -140,6 +160,8 @@
private bool multipleChildrenBeginDelayedError;
private bool twoErrorsInChild;

private bool throwMultipleErrorsForeach;

void EventHandlerErrorSync()
=> throw new InvalidTimeZoneException("Synchronous error from event handler");

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<h2>Subcomponent</h2>

@code {
protected override void OnInitialized()
{
throw new Exception("Error from foreach child");
}
}
Loading