aspnetcore icon indicating copy to clipboard operation
aspnetcore copied to clipboard

ErrorBoundary does not work

Open garrettlondon1 opened this issue 1 year ago • 20 comments

Is there an existing issue for this?

  • [X] I have searched the existing issues

Describe the bug

When creating a new Blazor Web App template and using InteractiveServer components without global interactivity.. I cannot get the error boundary to show

In a static component with an interactive island, it does not show Error content.. in a interactive page, it doesnt show error content either..

Confused what I am doing wrong

Expected Behavior

Error boundary shows child content or error content when an unhandled exception happens in a static page or interactive page/component

  • https://github.com/garrettlondon1/errorboundarytest

Created from Blazor Web app

Steps To Reproduce

Go to the home page, and click the button to increase the count

Exceptions (if any)

warn: Microsoft.AspNetCore.Components.Server.Circuits.RemoteRenderer[100] Unhandled exception rendering component: Exception of type 'System.Exception' was thrown. System.Exception: Exception of type 'System.Exception' was thrown. at StaticErrorBoundary.Components.Pages.Counter.IncrementCount() in /Users/garrettlondon/Github/StaticErrorBoundary/Components/Pages/Counter.razor:line 29 at Microsoft.AspNetCore.Components.EventCallbackWorkItem.InvokeAsync[T](MulticastDelegate delegate, T arg) at Microsoft.AspNetCore.Components.ComponentBase.Microsoft.AspNetCore.Components.IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, Object arg) at Microsoft.AspNetCore.Components.RenderTree.Renderer.DispatchEventAsync(UInt64 eventHandlerId, EventFieldInfo fieldInfo, EventArgs eventArgs, Boolean waitForQuiescence) fail: Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost[111]

Unhandled exception in circuit 'XOj9HyZGUCrv2y5bZvv5Mswe399H9w8ayWexTB5H--w'. System.Exception: Exception of type 'System.Exception' was thrown. at StaticErrorBoundary.Components.Pages.Counter.IncrementCount() in /Users/garrettlondon/Github/StaticErrorBoundary/Components/Pages/Counter.razor:line 29 at Microsoft.AspNetCore.Components.EventCallbackWorkItem.InvokeAsync[T](MulticastDelegate delegate, T arg) at Microsoft.AspNetCore.Components.ComponentBase.Microsoft.AspNetCore.Components.IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, Object arg) at Microsoft.AspNetCore.Components.RenderTree.Renderer.DispatchEventAsync(UInt64 eventHandlerId, EventFieldInfo fieldInfo, EventArgs eventArgs, Boolean waitForQuiescence)

.NET Version

8.0.302

Anything else?

No response

garrettlondon1 avatar Jun 23 '24 20:06 garrettlondon1

Thanks for reaching out, @garrettlondon1.

It looks like the reason the error boundary doesn't show is because the error boundary is being rendered as a child component of the component throwing the exception. Since exceptions only bubble up the component hierarchy, a child error boundary component won't catch that exception.

This behavior could be changed, but we're going to backlog this for now to collect more feedback.

MackinnonBuck avatar Jun 24 '24 16:06 MackinnonBuck

It looks like the reason the error boundary doesn't show is because the error boundary is being rendered as a child component of the component throwing the exception.

Thanks @MackinnonBuck, but I'm confused.

Counter.razor code block has the method IncrementCount Counter.razor markup has the onclick handler that calls IncrementCount

This interactivity lives within the same exact component, no other components are throwing an exception. Why would an error boundary not work on the Counter.razor page?

What am I missing here?

garrettlondon1 avatar Jun 24 '24 19:06 garrettlondon1

And what is the solution to catch these unhandled exceptions without putting a try catch block in every single point of interactivity (which is crazy at scale)

garrettlondon1 avatar Jun 24 '24 19:06 garrettlondon1

Hello guys,

I've got two examples:

  1. This example contains case when error is caught. There are 2 components: 1st rises error and seconds wraps 1st with ErrorBoundary

ComponentWithError.razor image

WrappingComponent.razor image

  1. This example contains case when error is not caugh and unhandled exception happens. image

I found the reson why errorboundary does not work when error happens just inside content but not in child component There is such a class .Microsoft.AspNetCore.Components.RenderTree.Renderer.cs which contains HandleExceptionViaErrorBoundary method that recursively tries to find ErrorBoundary to handle exception. But search goes in tree by components. image

That's why my first case works. Because WrappingComponent.razor contains ErrorBoundary But second case has no wrapping component or that component just does not have ErrorBoundary . ErrorBoundary is located just inside that component. And that case is not covered by that loop in Renderer.cs

Seems like we are obliged always to wrap components with ErrorBoundary but cannot just wrap some content.

Any reason for that ??? Why whole tree cannot be checked ??

IevgeniiKunshchykov avatar Jun 25 '24 13:06 IevgeniiKunshchykov

@MackinnonBuck @danroth27 I see three options that Blazor forces developers into:

  • Use global interactivity with Error Boundary > not an option, cannot take advantage of SSR and per-page interactivity

  • Wrap every interactive component inside another page entirely so that Renderer.cs can see ErrorBoundary > terrible design

  • Use a try catch block to handle exceptions in every single interactive method > not scalable at all

Thank you @IevgeniiKunshchykov

garrettlondon1 avatar Jun 25 '24 13:06 garrettlondon1

I believe @IevgeniiKunshchykov's assessment matches are our understanding of the issue. We could make a change to make it possible for error boundaries to handle exceptions thrown by HTML event handlers. This would be a behavior change that could potentially be breaking, but it seems likely that the impact could be negligible. But before we make such a change, we want to make sure we have sufficient justification and user demand for the change. We don't want to risk breaking existing users if the beneficial impact is unclear.

@garrettlondon1 If we were to make this change, are you saying that error boundaries would then meet your error handling needs? Or are you saying in your last comment that you think Blazor needs an entirely different error handling solution?

danroth27 avatar Jun 25 '24 14:06 danroth27

@danroth27 thanks for the quick reply..

ErrorBoundary is definitely the proper solution for Interactive blazor error handling

The problem is that you have to wrap every single interactive component in it because App.razor/Router is now static

  • This is fairly manageable and im fine with it..

All this story is asking for, is to make every interactive action inside of the ErrorBoundary able to bubble up the component tree correctly, and hit the ErrorBoundary properly: not throw an unhandled exception

  • Without the hack that @IevgeniiKunshchykov did with WrappingComponent

garrettlondon1 avatar Jun 25 '24 15:06 garrettlondon1

BTW ... I think we already cover that the error boundary should be around the portion of the component hierarchy that can throw an exception. We don't say anywhere that it can directly wrap content. Mackinnon and I spent some time working on the coverage, and we're taking feedback, but I think what's being discussed here is covered.

https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/handle-errors?view=aspnetcore-8.0#error-boundaries

UPDATE (7/18): After discussion with @TimMurphy and @MackinnonBuck, I made improvements to our coverage to at least help clear up confusion. The updated section is live now at ...

https://learn.microsoft.com/aspnet/core/blazor/fundamentals/handle-errors#error-boundaries

guardrex avatar Jul 11 '24 23:07 guardrex

Thanks @guardrex, but the example used in the docs is a very poor example, especially when it relates to this issue.

Screenshot 2024-07-27 at 11 59 38 AM

In this photo, why would you ever make the Home component Interactive?

  • It seems like the docs are trying to avoid the fact that the ErrorBoundary is just not implemented correctly

If the Home component was not interactive (which it should NOT be), and the Counter component was interactive. Then the error boundary in the docs would not work. EVEN if the Counter is wrapped in an ErrorBoundary also

Why are we letting every individual developer figure this out painfully on their own when migrating to SSR, instead of either clarifying the docs or fixing the underlying issue?

garrettlondon1 avatar Jul 27 '24 16:07 garrettlondon1

I would add to the docs an example where:

  • Home.razor is static, does not wrap EmbeddedCounter in an ErrorBoundary

  • EmbeddedCounter is Interactive, wrapped in an ErrorBoundary

Explain, why this does not work with @onclick handlers Explain, why this does work if the event originates from another subcomponent

garrettlondon1 avatar Jul 27 '24 16:07 garrettlondon1

I don't agree with your feedback (yet 😄), so I yield to @MackinnonBuck and @danroth27 to determine if further changes are needed. If not, we'll continue to take feedback and see if others find it confusing.

Most of what you're discussing is covered prior to that example. For analysis, @MackinnonBuck and @danroth27 should see the full section ...

https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/handle-errors?view=aspnetcore-8.0#error-boundaries

guardrex avatar Jul 27 '24 17:07 guardrex

I can confirm @guardrex that we also find it confusing. We are looking for a reliable way to log Exceptions from a standalone Blazor Web Assembly Application to a Server within a Production Environment. Sources indicate that using the OnErrorAsync Method of the ErrorBoundary Component as a "global exception handler" is a way of achieving this. On closer inspection, however, it seems that Exceptions thrown from an HTML OnClick Event are not reliably handled by the ErrorBoundary Component. For other Developers looking to do something similar then it may be helpful for Microsoft to add a Note to this effect to their Documentation to hopefully minimise wasted effort going forwards. Having given this considerable thought we think the best way forwards for us, until Microsoft comes up with a better solution, is to remove the ErrorBoundary Component from our Application. @danroth27 in response to your Question to @garrettlondon1 ideally we would like a Component that provides a reliable way of handling Exceptions globally rather than adding the same code multiple times to Try...Catch blocks, which seems to be the only solution as it stands.

cwoodward-taurus avatar Aug 30 '24 07:08 cwoodward-taurus

@cwoodward-taurus ... I'm going to enhance the coverage with additional guidance :point_up:. However, I don't have product unit remarks yet on the main question that you're asking about outside of just remarking that ErrorBoundary no-ops on them ...

Exceptions thrown from an HTML OnClick Event are not reliably handled by the ErrorBoundary Component

Article remarks for that scenario will need to come from the product unit engineers here. I'll include anything else they want to say about it when I work that issue, and I should reach that issue next week after we get back from the Labor Day holiday.

guardrex avatar Aug 30 '24 12:08 guardrex

@cwoodward-taurus I want to note there is a solution. You need to create another wrapper on top of every interactive component you have..

@IevgeniiKunshchykov example calls it WrappingComponent

Page.razor (static)

  • SafeWrappingComponent.razor
  • View.razor

Its incredibly painful, but it works for all scenarios. Now that I'm thinking.. I wonder if you can make an interactive component that wraps interactive ChildContent.

Almost, wrapping ErrorBoundary itself inside of another interactive component which takes a ChildContent parameter. Will try it out.

  • Nope, Interactive components cant be serialized as ChildContent in static pages, even if its wrapped in another static component

garrettlondon1 avatar Aug 31 '24 02:08 garrettlondon1

I am using blazor interactive components in MVC project and the fact that I have to wrap every component I create in another component just to catch errors is a bit frustrating, to say the least. Or is there another way to globally catch errors in such a case?

clarity99 avatar Sep 06 '24 11:09 clarity99

Can we get this pulled out of the backlog?

garrettlondon1 avatar Sep 19 '24 21:09 garrettlondon1

Here is one more case with unexpected behavior of errorboundary which is crucial for us. We've solved the problem mentioned above with wrappers around our components. Here is example: Image

But when internal component has some state, like here I have counter with some value: Image

And when exception is thrown and caught by errorboundary then internal component of errorboundary is just recreated and state is lost: Image

Can this be solved somehow? Because it seems each approach of using errorboudary has some holes :(

IevgeniiKunshchykov avatar Oct 17 '24 15:10 IevgeniiKunshchykov

@danroth27 there doesn't look like there is currently a good solution to ErrorBoundary... regarding above comment.

Even if the error boundary keeps child content in the view, and does not use custom ErrorContent RenderFragment, all state will be cleared on error with WrappingComponent approach.

garrettlondon1 avatar Oct 17 '24 16:10 garrettlondon1

It would be nice to be able to include the ErrorBoundary within the component I want to handle itself. Now, as others pointed out, I have to hoist up the ErrorBoundary a level instead of having a nicely encapsulated component.

Quite frankly, the inflexibility of it makes it useless for most scenarios where I would have wanted to use it.

codymullins avatar Oct 18 '24 16:10 codymullins

To elaborate further. Here's a real scenario where this ErrorBoundary does work:

<LoggingErrorBoundary>
    <ChildContent>
        <ServiceStatusTile Service="@svc"/>
    </ChildContent>
    <ErrorContent>
        <div class="bg-red-500 text-white p-3 rounded-lg">
            <h3 class="font-bold">An error occurred</h3>
        </div>
    </ErrorContent>
</LoggingErrorBoundary>

Fine, this renders the error content. However, it totally wipes out the component: Image

Compare this to the happy path, where the components all render: Image

Now, our ServiceStatusTile is a single component. What if we'd want to have an error boundary in the component itself and potentially render different parts of the component still when there's an error? We can't wrap arbitrary code, so we can't do that without these component wrappers that @garrettlondon1 mentioned. Huge pain, not maintainable.

codymullins avatar Oct 18 '24 16:10 codymullins

Having issues as well.

Exceptions thrown from an HTML OnClick Event are not reliably handled by the ErrorBoundary Component

Not sure if this applies here, but had a case where an @ sign was forgotten. While exceptions with <span onclick="SomeMethodThatThrowsEx">Click here</span> were not handled by the error boundary, exceptions with <span @onclick="SomeMethodThatThrowsEx">Click here</span> were handled.

rklfss avatar Feb 12 '25 09:02 rklfss

Having issues as well.

Exceptions thrown from an HTML OnClick Event are not reliably handled by the ErrorBoundary Component

Not sure if this applies here, but had a case where an @ sign was forgotten. While exceptions with <span onclick="SomeMethodThatThrowsEx">Click here</span> were not handled by the error boundary, exceptions with <span @onclick="SomeMethodThatThrowsEx">Click here</span> were handled.

My guess is that fired a js onclick event instead of a blazor onclick event, so that makes sense

garrettlondon1 avatar Feb 12 '25 18:02 garrettlondon1

Image

Blazor also doesnt catch errors in Dispose methods.. for example if a jsInterop method fails in DisposeAsync, error boundary will not catch it and whole application will break.

garrettlondon1 avatar May 27 '25 20:05 garrettlondon1

I hit this issue with InteractiveServer page component yesterday too. The current solution in this issue is manually wrapping every InteractiveServer page component with your custom ErrorBoundary component. This was not acceptable for me. So i was looking for a different workaround for this problem. By knowing, if ErrorBoundary is on the root of every page, it will work, i proceeded. So i was looking for a way to automatically surround the page content with my CustomErrorBoundary component. My first attempt was by subclassing the page type on runtime. Sadly some callbacks were by passing this approach. But this was not needed anyway. The new version just wraps the page type inside a generic wrapper type for the RouteView.

You can see my current workaround in this gist: https://gist.github.com/Yarith/a524e5fc5fd89e356642f299d07f4209

Yarith avatar Jul 01 '25 09:07 Yarith