feat: report exceptions from Tasks that were never awaited - #213
Open
bobbyg603 wants to merge 2 commits into
Open
feat: report exceptions from Tasks that were never awaited#213bobbyg603 wants to merge 2 commits into
bobbyg603 wants to merge 2 commits into
Conversation
A Task that faults with nobody awaiting it never writes to Unity's log, so neither logMessageReceived nor logMessageReceivedThreaded ever sees it — these exceptions were invisible to the SDK entirely. The manager now subscribes to TaskScheduler.UnobservedTaskException and routes them through the same bounded queue and main-thread drain the background thread path already uses, since the event fires on the finalizer thread where no Unity API is safe. SetObserved is deliberately not called: marking the exception observed would suppress whatever the application does with it next, and reporting a failure must not change whether that failure happens. The AggregateException is flattened and each inner exception reported separately so unrelated failures land in separate dashboard buckets rather than collapsing into one wrapper. The sample previously worked around the gap by subscribing to the same event itself and re-raising on the main thread. That has to go, or every unobserved Task exception would report twice — which also retires the menu's main-thread dispatcher, now unused, and the row's KNOWN GAP styling. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds first-class reporting for exceptions from faulted Tasks that were never awaited by subscribing to TaskScheduler.UnobservedTaskException in BugSplatManager and routing those exceptions through the existing off-main-thread bounded queue + main-thread drain, making previously-invisible async failures observable by the SDK.
Changes:
- Subscribe to
TaskScheduler.UnobservedTaskExceptionbehind a newcaptureUnobservedTaskExceptionstoggle (default on) and enqueue flattened inner exceptions for reporting on the main thread. - Extend tests to cover unobserved-task exception enqueue behavior and add play-mode coverage for end-to-end runtime event capture (inconclusive on GC non-collection).
- Remove the sample’s prior workaround subscription to avoid double-reporting; update sample UI text and README documentation accordingly.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| Runtime/Manager/BugSplatManager.cs | Adds unobserved-task capture, shares the off-main-thread queue, and updates drop-warning wording. |
| Tests/Runtime/Manager/BackgroundLogMessageQueueTest.cs | Adds deterministic unit tests for AggregateException flattening/fan-out and safety cases. |
| Tests/Runtime/Manager/BugSplatManagerTest.cs | Adds play-mode tests for unobserved-task capture (with GC timing caveats). |
| Samples~/my-unity-crasher/Scripts/CrashScenarios.cs | Updates the “Unobserved Task exception” scenario text to reflect new SDK behavior. |
| Samples~/my-unity-crasher/Scripts/CrashScenarioMenu.cs | Removes the sample’s TaskScheduler.UnobservedTaskException subscription and main-thread dispatch pump to prevent double reporting. |
| Samples~/my-unity-crasher/README.md | Updates sample documentation to reflect that unobserved task exceptions are now captured by the SDK. |
| README.md | Documents the new unobserved-task exception behavior, defaults, and timing limitations. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+138
to
+143
| /// <summary> | ||
| /// Queues one report per faulted Task. Runs on the finalizer thread, so it only writes to | ||
| /// the queue; <see cref="Update"/> posts from the main thread. Reporting each inner | ||
| /// exception separately rather than the AggregateException wrapper keeps distinct failures | ||
| /// in distinct dashboard buckets. | ||
| /// </summary> |
The sample throws exceptions by design, so Error Pause halts play mode on every managed scenario. That reads as the player crashing rather than surviving — and with Maximize on Play it can drop the Game view back to its docked size, making the menu look like it disappeared while the Play button stays lit. Cost real debugging time before anyone thought to check the Console toolbar. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
daveplunkett
approved these changes
Aug 12, 2026
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.
Closes #212
What
A
Taskthat faults with nobody awaiting it never writes to Unity's log, so neitherlogMessageReceivednorlogMessageReceivedThreadedever sees it — these exceptions were invisible to the SDK entirely.BugSplatManagernow subscribes toTaskScheduler.UnobservedTaskExceptionand routes them through the same bounded queue and main-thread drain that #126 built for background-thread exceptions, since the event fires on the finalizer thread where no Unity API is safe.New toggle: Capture Unobserved Task Exceptions on
BugSplatManager, on by default, nested under Register Log Message Received like its sibling.Design decisions worth reviewing
SetObserved()is deliberately not called. Marking the exception observed would suppress whatever the application does with it next — including the process-terminating behavior a project can opt into viaThrowUnobservedTaskExceptions. Reporting a failure must not change whether that failure happens. The cost is that we report and then let the runtime do what it would have done anyway.The
AggregateExceptionis flattened and each inner exception reported separately. A Task awaiting other Tasks faults with aggregates inside aggregates; reporting the wrapper would bucket unrelated failures together in the dashboard. An empty aggregate (no inners) falls back to reporting the wrapper so nothing is silently dropped.The queue is shared with the background-thread path. It is created when either toggle is on, so enabling only this one still works. The drop warning was reworded from "background thread exception(s)" to "off-main-thread exception(s)" now that two sources feed it.
Timing is inherently late and not guaranteed. The runtime raises this event only when a GC collects the faulted Task, so a Task that is never collected is never reported. That is a property of the .NET event, not something the SDK can fix; it is documented in the README and the tooltip rather than papered over.
Sample
The sample had been working around this gap by subscribing to
TaskScheduler.UnobservedTaskExceptionitself and re-raising on the main thread. That had to go, or every unobserved Task exception would now report twice. Removing it also retires the menu'sOnMainThreadmain-thread dispatcher (the re-raise was its only caller), which is dropped fromICrashScenarioHostalong with the now-unusedConcurrentQueueandUpdatepump.The "Unobserved Task exception" row loses its
KnownGapgrey and its description now names the real capture path. This resolves a genuine UI confusion: the row was grey (meaning "the SDK does not capture this") while still producing a report (because the sample worked around it), which read as the menu lying.Verification
dotnet buildofRuntime/**+Tests/Runtime/**against Unity 6000.5.6f1 assemblies: 0 errors (2 pre-existingCS0649warnings inBugSplat.cs).dotnet testof the plain-NUnit queue suite: 22/22 passed, including 5 new cases covering per-inner-exception fan-out, nested-aggregate flattening, nullStackTrace→ empty string, the empty-aggregate fallback, and null-queue/null-exception safety.dotnet buildofSamples~against UnityEngine + TextMeshPro + UGUI: 0 errors — worth doing explicitly here becauseSamples~is tilde-excluded from compilation, so CI does not cover the interface change.Not verified: the Unity test runner was not executed locally (no licensed editor session); CI covers it. The two new
[UnityTest]cases inBugSplatManagerTestexercise the real event end to end, and because the runtime cannot be forced to collect the faulted Task, they report inconclusive rather than failing if the GC declines — a flaky red build would be worse than a missing signal. Real-device behavior is deferred to the 5.0.0 smoke test (#200).🤖 Generated with Claude Code