Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
a6b40e9
Name the component, not its element, in RenderError
azchohfi Oct 2, 2026
3404692
Report render errors an ErrorBoundary catches
azchohfi Oct 2, 2026
6728ac4
Cover RenderError on func/memo mount, nested boundary throws, and bot…
azchohfi Oct 5, 2026
96b4ab9
Cache generic component names and skip ComponentUnmount naming when t…
azchohfi Oct 5, 2026
d348e74
CHANGELOG: RenderError names the component and reports every render e…
azchohfi Oct 5, 2026
fe20e8f
Record the render-error catches in the spec-044 swallowed-error ledger
azchohfi Oct 5, 2026
5f95751
Don't read Exception.Message when emitting RenderError
azchohfi Oct 5, 2026
b1c100e
Report every exception an ErrorBoundary recovers from; fix stale attr…
azchohfi Oct 5, 2026
d8496be
Report OOM/SO render failures outside a boundary too
azchohfi Oct 6, 2026
4075055
Merge origin/main into azchohfi-render-error-component-name
azchohfi Oct 7, 2026
9ee41ea
Report root effect failures; name rollback unmounts like other unmounts
azchohfi Oct 7, 2026
0469209
Attribute root failures to the root that threw; format generic array …
azchohfi Oct 7, 2026
10f0fdb
Merge origin/main into azchohfi-render-error-component-name
azchohfi Oct 7, 2026
ef58dd2
Name derived function/memo elements by their own type on mount
azchohfi Oct 7, 2026
288575b
Merge remote-tracking branch 'origin/main' into azchohfi-render-error…
azchohfi Oct 7, 2026
b585258
Format generic derived element names like any other generic component
azchohfi Oct 7, 2026
918ab90
Merge remote-tracking branch 'origin/main' into azchohfi-render-error…
azchohfi Oct 7, 2026
3cc6999
Use System.Threading.Lock for the host registry gate (IDE0330)
azchohfi Oct 7, 2026
671fed2
Merge remote-tracking branch 'origin/main' into azchohfi-render-error…
azchohfi Oct 7, 2026
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
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,25 @@ Conventions for contributors:

### Fixed

- **`RenderError` names the component that threw, and now reports every render
error** (issue #1321, spec 044 §6.2.1). For class components the event reported
Comment thread
azchohfi marked this conversation as resolved.
the element record (``ComponentElement`1`` / `ComponentElement`) instead of the
component unless the `Render` keyword was also on, so an Errors-only listener
such as `winapp devtools logs` could not tell which component failed. It now
reports the component's type name (`Counter`; a generic one as `Foo<Int32>`),
and the other per-component events agree on it. **Listeners now also receive
errors that were previously silent:** a component (class, function or memo)
throwing on its first render, a `ReactorHost` / `ReactorHostControl` root
component or render function throwing (in `Render()` or in its effects), and any
render error an `ErrorBoundary`
catches — reported once, at the throw site, even though the app shows the
fallback. Same event, same payload; the message stays redacted. Generic
component names change from ``Foo`1`` to `Foo<Int32>` in every component event.
For these component failures the event is emitted before any `RenderErrorHandler`
runs, so it fires whether the handler replaces the fallback or calls `Propagate()` —
once per throw either way. `Reconcile` and `Cleanup` failures are not render errors
and are not reported on it.

- **DataGrid inline editors no longer shift the cell's text when editing starts**
(issue #1340). The built-in TextBox and NumberBox editors used `.Padding(2)` inside
WinUI's 32px minimum height, which draws text at the top, so the text moved about 4px
Expand Down
2 changes: 1 addition & 1 deletion docs/_pipeline/diagrams/source-mapping/attribution.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions docs/_pipeline/templates/advanced.md.dt
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,16 @@ failing child, once per host render for a host-level failure. De-duplicate side
effects such as telemetry, for example by remembering the exceptions you have
already recorded.

Independently of the handler, every component failure — a `Render()` or an
effect flush that throws, at any depth including the host's root — is also
reported on the `Microsoft-UI-Reactor` ETW `RenderError` event (`Errors`
keyword), naming the component — see [diagnostics](diagnostics.md). The event is
emitted before the handler runs, so it fires once per throw whatever the handler
does, including when it calls `Propagate()`. Errors an `ErrorBoundary` catches
are reported there too, even though they never reach the handler. `Reconcile`
and `Cleanup` failures have no failing render and are not reported on that
event.

`Cleanup` reports an effect cleanup that threw while a host was being
disposed — a window closing, or a `ReactorHostControl` being disposed. There
is nothing left to display, so the return value is ignored and the handler only
Expand Down
15 changes: 10 additions & 5 deletions docs/_pipeline/templates/source-mapping.md.dt
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,20 @@ consumers can subscribe to just the render channel:
```csharp snippet="source:src/Reactor/Core/Diagnostics/ReactorEventSource.cs#etw-keywords"
```

`ComponentRenderStart` / `ComponentRenderStop` fire with
`componentName = node.Component?.GetType().Name`. That string is the
`ComponentRenderStart` / `ComponentRenderStop` fire with the component's
type name — `Counter` for a `Counter : Component<CounterProps>`, whichever
factory created it. That string is the
attribution token that flows into PerfView / `dotnet-trace` /
`xperf`, and it is the same string [`devtools-internals`](devtools-internals.md)
uses to label overlay frames. Per-component, not per-element — but
sufficient for the common question "which component is re-rendering
on every tick".
on every tick". `RenderError`, `EffectsFlushStart` / `Stop` and
`ComponentUnmount` use the same name. A generic component reports its type
arguments in C# form (`ItemList<Int32>`, not ``ItemList`1``); function and memo
components, which have no type of their own, report `FuncElement` and
`MemoElement`.

![Source attribution: component-name attribution flows Component.GetType().Name into ETW, while per-element attribution stamps each DSL call site with file and line via the interceptor generator, readable from any realized control through ReactorSourceMap.GetSource.](images/source-mapping/attribution.svg)
![Source attribution: component-name attribution flows the component's type name, formatted by ComponentNames (generic components in C# form), into ETW, while per-element attribution stamps each DSL call site with file and line via the interceptor generator, readable from any realized control through ReactorSourceMap.GetSource.](images/source-mapping/attribution.svg)

## Reconcile-pass attribution

Expand All @@ -67,7 +72,7 @@ on every tick".
| `ReconcileStart` / `Stop` | Root element type + diff counters | ETW `Reconcile` keyword |
| `EffectsFlushStart` / `Stop` | Component CLR type name | ETW `Render` keyword |
| `StateChange` | Hook kind + value type | ETW `State` keyword |
| `RenderError` | Component name + exception type only (message redacted) | ETW `Errors` keyword |
| `RenderError` | Component name + exception type only (message redacted); emitted when a component's `Render()` or effect flush throws — on mount, update and at the host root — including errors an `ErrorBoundary` catches (named at the throw site, once) | ETW `Errors` keyword |
| Per-element file:line | Element call site | `Element.CallSite` / `ReactorSourceMap.GetSource` (when source mapping is enabled at build time) |

The reconcile pass also emits a counter summary on stop:
Expand Down
10 changes: 10 additions & 0 deletions docs/guide/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,16 @@ failing child, once per host render for a host-level failure. De-duplicate side
effects such as telemetry, for example by remembering the exceptions you have
already recorded.

Independently of the handler, every component failure — a `Render()` or an
effect flush that throws, at any depth including the host's root — is also
reported on the `Microsoft-UI-Reactor` ETW `RenderError` event (`Errors`
keyword), naming the component — see [diagnostics](diagnostics.md). The event is
emitted before the handler runs, so it fires once per throw whatever the handler
does, including when it calls `Propagate()`. Errors an `ErrorBoundary` catches
are reported there too, even though they never reach the handler. `Reconcile`
and `Cleanup` failures have no failing render and are not reported on that
event.

`Cleanup` reports an effect cleanup that threw while a host was being
disposed — a window closing, or a `ReactorHostControl` being disposed. There
is nothing left to display, so the return value is ignored and the handler only
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/images/source-mapping/attribution.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 10 additions & 5 deletions docs/guide/source-mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,20 @@ public static class Keywords
}
```

`ComponentRenderStart` / `ComponentRenderStop` fire with
`componentName = node.Component?.GetType().Name`. That string is the
`ComponentRenderStart` / `ComponentRenderStop` fire with the component's
type name — `Counter` for a `Counter : Component<CounterProps>`, whichever
factory created it. That string is the
attribution token that flows into PerfView / `dotnet-trace` /
`xperf`, and it is the same string [`devtools-internals`](devtools-internals.md)
uses to label overlay frames. Per-component, not per-element — but
sufficient for the common question "which component is re-rendering
on every tick".
on every tick". `RenderError`, `EffectsFlushStart` / `Stop` and
`ComponentUnmount` use the same name. A generic component reports its type
arguments in C# form (`ItemList<Int32>`, not ``ItemList`1``); function and memo
components, which have no type of their own, report `FuncElement` and
`MemoElement`.

![Source attribution: component-name attribution flows Component.GetType().Name into ETW, while per-element attribution stamps each DSL call site with file and line via the interceptor generator, readable from any realized control through ReactorSourceMap.GetSource.](images/source-mapping/attribution.svg)
![Source attribution: component-name attribution flows the component's type name, formatted by ComponentNames (generic components in C# form), into ETW, while per-element attribution stamps each DSL call site with file and line via the interceptor generator, readable from any realized control through ReactorSourceMap.GetSource.](images/source-mapping/attribution.svg)

## Reconcile-pass attribution

Expand All @@ -74,7 +79,7 @@ on every tick".
| `ReconcileStart` / `Stop` | Root element type + diff counters | ETW `Reconcile` keyword |
| `EffectsFlushStart` / `Stop` | Component CLR type name | ETW `Render` keyword |
| `StateChange` | Hook kind + value type | ETW `State` keyword |
| `RenderError` | Component name + exception type only (message redacted) | ETW `Errors` keyword |
| `RenderError` | Component name + exception type only (message redacted); emitted when a component's `Render()` or effect flush throws — on mount, update and at the host root — including errors an `ErrorBoundary` catches (named at the throw site, once) | ETW `Errors` keyword |
| Per-element file:line | Element call site | `Element.CallSite` / `ReactorSourceMap.GetSource` (when source mapping is enabled at build time) |

The reconcile pass also emits a counter summary on stop:
Expand Down
26 changes: 22 additions & 4 deletions docs/specs/044/swallowed-error-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,12 @@ row gains a column and the gate rejects it.

| Verdict | Sites | Shipped | Deferred |
|---|---|---|---|
| `Keep` | 24 | 24 | 0 |
| `Keep` | 34 | 34 | 0 |
| `Narrow` | 38 | 38 | 0 |
| `Propagate` | 7 | 7 | 0 |
| `TryFinally` | 7 | 7 | 0 |
| `TryXxx` | 10 | 0 | 10 |
| `PromoteEvent` | 26 | 18 | 8 |
| `PromoteEvent` | 30 | 22 | 8 |
| `Deleted` | 7 | 7 | 0 |
| `Trace` | 5 | 5 | 0 |

Expand Down Expand Up @@ -253,7 +253,7 @@ file that exists, unless the heading carries a `(retired …)` marker.
| `EndOfStreamException` reject | 1 | `PromoteEvent` | shipped | Same. |
| Outer catches — `Capture`, `Restore` | 2 | `Narrow` | shipped | Narrowed to `IOException`. One clause each on the capture and restore paths; the original entry said "Outer catches" without a count. |

### `src/Reactor/Core/Reconciler.cs` — Phase C.7b (commit `054c53ef`) + Phase C.8 (commit `21cd6ef9`)
### `src/Reactor/Core/Reconciler.cs` — Phase C.7b (commit `054c53ef`) + Phase C.8 (commit `21cd6ef9`) + render-error reporting (issue #1321)

| Site(s) | Sites | Verdict | Status | Notes |
|---|---|---|---|---|
Expand All @@ -263,12 +263,16 @@ file that exists, unless the heading carries a `(retired …)` marker.
| ConnectedAnimation `GetAnimation` | 1 | `Keep` | shipped | **fail-safe-to-default.** Same. |
| ConnectedAnimation `TryStart` | 1 | `Keep` | shipped | **fail-safe-to-default.** Same. |
| `ApplyThemeBindings` | 1 | `Keep` | shipped | **fail-safe-to-default.** LogCategory.Theme — the catch wraps a XAML `Style.Load` compile. Could narrow to `XamlParseException` in a follow-up. |
| `ReconcileComponent` render catch outside any `ErrorBoundary` (`_errorBoundaryDepth == 0`) | 1 | `Keep` | shipped | **user-callback isolation** per §6.7.3 — the `try` wraps the app's `Render()` and the effect flush, so the failure class is arbitrary app code and cannot be narrowed. The render is replaced by `ErrorFallback`, and the failure is reported through `ILogger` and the typed `RenderError` event, naming the component (issue #1321, PR #1323), then handed to the app's `RenderErrorHandler` when one is set (issue #1291). `OutOfMemoryException` / `StackOverflowException` are filtered out and propagate (after the report-and-rethrow arm has reported them). Pre-dates the ledger; adjudicated when #1321 touched it. |
| `ReconcileComponent` report-and-rethrow arm — inside an `ErrorBoundary` (`_errorBoundaryDepth > 0`), or `OutOfMemoryException` / `StackOverflowException` anywhere | 1 | `PromoteEvent` | shipped | Report-and-rethrow, **not a swallow**: emits `RenderError` naming the component that threw — the only frame that knows it — then `throw;` rethrows unchanged to the enclosing boundary, which renders its fallback and does not report again (one event per throw). No exception-type filter — not even the `OutOfMemoryException` / `StackOverflowException` carve-out — because the boundary's own `catch` takes every exception, so every exception it recovers from is reported. Outside a boundary the arm also takes OOM/SO, which skip the fallback arm and reach the host's outer `catch` — itself a recovery — so they are reported once here too. Like every Reactor catch site since issue #1291, it lets an exception the app declined via `RenderError.Propagate()` pass (`!RenderErrorDispatch.IsPropagating(ex)`): its throw site already reported it. Added by issue #1321 / PR #1323. |

### `src/Reactor/Core/Reconciler.Mount.cs` — Phase C.8 (commit `21cd6ef9`)
### `src/Reactor/Core/Reconciler.Mount.cs` — Phase C.8 (commit `21cd6ef9`) + render-error reporting (issue #1321)

| Site(s) | Sites | Verdict | Status | Notes |
|---|---|---|---|---|
| `ContentDialog.ShowAsync + OnClosed` | 1 | `Keep` | shipped | **user-callback isolation** per §6.7.3 — the try wraps both `ShowAsync` AND the user-supplied `OnClosed` delegate. Cannot narrow without splitting the try-catch into two; deferred. |
| First-render catches outside any `ErrorBoundary` (`_errorBoundaryDepth == 0`) — `MountComponent`, `MountFuncComponent`, `MountMemoComponent` | 3 | `Keep` | shipped | **user-callback isolation** per §6.7.3 — each `try` wraps the app's first `Render()` (class, function or memo component) and its effect flush; arbitrary app code, so no narrowing. The render is replaced by `ErrorFallback` and reported through `ILogger` and, since issue #1321 (PR #1323), the typed `RenderError` event — these arms previously only logged — then handed to the app's `RenderErrorHandler` when one is set (issue #1291). `OutOfMemoryException` / `StackOverflowException` propagate (after the report-and-rethrow arm has reported them). Pre-date the ledger; adjudicated when #1321 touched them. |
| First-render report-and-rethrow arms — inside an `ErrorBoundary` (`_errorBoundaryDepth > 0`), or `OutOfMemoryException` / `StackOverflowException` anywhere — `MountComponent`, `MountFuncComponent`, `MountMemoComponent` | 3 | `PromoteEvent` | shipped | Report-and-rethrow, **not a swallow**: emits `RenderError` naming the component that threw, then `throw;` rethrows unchanged to the enclosing boundary, which does not report again (one event per throw). Same filter as the update-path arm: every exception inside a boundary, plus OOM/SO outside one (the host recovers from those too), minus a declined propagation. Added by issue #1321 / PR #1323. |

### `src/Reactor/Core/RenderContext.cs` — Phase C.6 (commit `90d516b0`) + Phase C.9 narrowing

Expand Down Expand Up @@ -307,6 +311,20 @@ types without making it a narrowing. Same shape as the existing convention at
| `CanResolvePageType` resolver probe | 1 | `Keep` | shipped | **fail-safe-to-default.** The method's contract is "true **only if** definitively resolvable". Any failure to answer means we cannot confirm, and returning `false` refuses the navigation — the safe direction, and the one that cannot produce the access violation. Expected types are `COMException` at the WinRT boundary and `InvalidOperationException` / `ArgumentException` from a generated or hand-written `IXamlMetadataProvider`; propagating anything else would convert a third-party provider's bug into a render-loop error **while the navigation is refused either way**, i.e. strictly worse for identical safety. `OutOfMemoryException` / `StackOverflowException` still propagate. |
| `TryNavigate` around `Frame.Navigate` | 1 | `Keep` | shipped | **user-callback isolation** (§6.7.3). What surfaces here is the **page constructor's** exception — arbitrary application code — and routing it into the element's declared `OnNavigationFailed` channel is the arm's entire purpose. Directly analogous to the `ContentDialog.ShowAsync + OnClosed` entry above. **Narrowing would reintroduce the defect this fix exists to remove:** an unanticipated page-constructor failure would escape the mount pass. **Coverage gap, stated rather than hidden:** reaching this arm needs a target that *resolves* but whose constructor then throws, i.e. a real `.xaml`-backed page. The selftest host ships no XAML, so every target it can offer is either refused before `Navigate` (code-only) or a framework type that constructs fine. The arm is therefore reasoned-about, not exercised — unlike the refusal path, which `FrameNav_CodeOnlyPageRefusedNotFatal` pins directly. |

### `src/Reactor/Hosting/ReactorHost.cs` — render-error reporting (issue #1321)

| Site(s) | Sites | Verdict | Status | Notes |
|---|---|---|---|---|
| Root render catches in `Render` — root component `Render()`, root render function | 2 | `Keep` | shipped | **user-callback isolation** per §6.7.3 — each `try` wraps the app's root render, arbitrary app code. The host shows its error fallback — or, since issue #1291, whatever the app's `RenderErrorHandler` returns — and keeps running; the failure is reported through `ILogger` and, since issue #1321 (PR #1323), the typed `RenderError` event — previously these arms only logged. The catch lets an exception the app declined via `RenderError.Propagate()` pass (`!RenderErrorDispatch.IsPropagating(ex)`, issue #1291). A `HookOrderException` during a hot-reload render is handled by the preceding, narrower clause. Pre-date the ledger; adjudicated when #1321 touched them. |
| Outer render-loop catch in `Render` ("Render FAILED") — reconcile, content install, root effect flush, post-render callbacks | 1 | `Keep` | shipped | **user-callback isolation** per §6.7.3 — the `try` spans the reconcile pass (child renders and effects), the root's effect flush and app callbacks such as `OnRenderComplete`, all of which run app code. The host shows its fallback (or the app's `RenderErrorHandler` result, issue #1291) and keeps running; an exception the app declined via `RenderError.Propagate()` passes (`!RenderErrorDispatch.IsPropagating(ex)`). When the failing phase is the root's effect flush, the root component owns the failure, so since issue #1321 (PR #1323) it is also reported on the typed `RenderError` event, like a child's effect-flush failure; other phases have no failing component and are not. Pre-dates the ledger; adjudicated when #1321 touched it. |

### `src/Reactor/Hosting/ReactorHostControl.cs` — render-error reporting (issue #1321)

| Site(s) | Sites | Verdict | Status | Notes |
|---|---|---|---|---|
| Root render catches in `Render` — root component `Render()`, root render function | 2 | `Keep` | shipped | **user-callback isolation** per §6.7.3. Same shape and reporting as `ReactorHost.cs` above. |
| Outer render-loop catch in `Render` ("Render FAILED") | 1 | `Keep` | shipped | **user-callback isolation** per §6.7.3. Same shape and reporting as `ReactorHost.cs` above. |

### `src/Reactor/Hosting/ReactorWindow.cs` — Phase C.8 (commit `21cd6ef9`) + Phase C.9 narrowing

Phase C.8 migrated the file's `Debug.WriteLine` calls to `DiagnosticLog` with
Expand Down
Loading
Loading