feat(ui): add global exception backstop - #148
Merged
Merged
Conversation
Nothing sat under the running dispatcher. Program.Main has a top-level try/catch, but it only wraps startup; once the Avalonia dispatcher is pumping, an unsubscribed ThrownExceptions, an unobserved fire-and-forget task fault, or a background-thread throw had no handler at all. The first crashed or vanished silently, the last died with no stack logged. Add GlobalExceptionBackstop, installed once at ReactiveUI build time (before the first ReactiveCommand is constructed), wiring three last-resort handlers: - the recoverable ReactiveUI handler logs the exception and reports a generic message to the panel, keeping the app alive (Debugger.Break in DEBUG when a debugger is attached); - TaskScheduler.UnobservedTaskException logs and marks the fault observed; - AppDomain.UnhandledException logs at Critical and flushes Serilog before the process dies, so a background-thread crash leaves a stack. The panel is resolved lazily at fire time so installing the handler does not construct a command before it is set. This is the last-resort ring only: it does not change the happy path and does not replace the per-command report+log ring.
mrcsin
marked this pull request as ready for review
July 27, 2026 15:49
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Nothing sat under the running dispatcher.
Program.Mainhas a top-level try/catch, but itonly wraps startup — once the Avalonia dispatcher is pumping, three failure classes had no
handler at all: an unsubscribed
ThrownExceptions, an unobserved fire-and-forget task fault,and a background-thread throw. The first crashed or vanished silently; the last died with no
stack in the log, leaving a field-reported background crash undiagnosable.
What changed
Add
GlobalExceptionBackstop, installed once at ReactiveUI build time (before the firstReactiveCommandis constructed), wiring three last-resort handlers:IReactiveUIBuilder.WithExceptionHandler): logs theexception and reports a generic message to the panel, keeping the app alive. In DEBUG with a
debugger attached it breaks. The observer body is wrapped so the terminal handler can never itself
crash the app.
TaskScheduler.UnobservedTaskException: logs Error and marks the fault observed.AppDomain.CurrentDomain.UnhandledException: logs at Critical (Serilog Fatal) — including anon-
Exceptionthrown object — and flushes Serilog before the process dies, so a background-threadcrash leaves a stack behind.
The panel is resolved lazily at fire time so installing the handler doesn't construct a command
before it's set. This is the last-resort ring only — it doesn't change the happy path and doesn't
replace the per-command report+log ring from PR-F1.
This is PR-F2 of the error/logging pipe roadmap. Follow-on: #113 (F3) rebased on F1+F2.
Note on ReactiveUI: the pipe roadmap was written against
RxApp.DefaultExceptionHandler, butthis project's ReactiveUI (23.2.28) removed
RxApp; the handler is set via the one-shotWithExceptionHandleratUseReactiveUIbuild time. Build-time registration is what guarantees the"before first command construction" ordering.
Verification
dotnet build SemiStep.slnx— 0 warnings (warnings are errors).dotnet test SemiStep/SemiStep.Tests/SemiStep.Tests.csproj— 1478 passed, 0 failed (+4 new tests).dotnet formatclean; Rider analyzer clean on the changed files.Debugger.Break, never-throw body, non-Exceptionfatal logging).Per-task commits before collapse