Skip to content

Fix UnobservedTaskException in TaskToAsyncResult when APM callback skips EndXxx#126165

Open
mad1081 wants to merge 1 commit intodotnet:mainfrom
mad1081:fix-unobserved-task-exception-apm
Open

Fix UnobservedTaskException in TaskToAsyncResult when APM callback skips EndXxx#126165
mad1081 wants to merge 1 commit intodotnet:mainfrom
mad1081:fix-unobserved-task-exception-apm

Conversation

@mad1081
Copy link

@mad1081 mad1081 commented Mar 26, 2026

Summary

On .NET 6+, NetworkStream.BeginRead (and any APM operation backed by TaskToAsyncResult) can
fire TaskScheduler.UnobservedTaskException when:

  1. The socket/stream is disposed while a read is pending (causing SocketException(995)), and
  2. The BeginRead callback does not call EndRead — a valid shutdown pattern where the callback
    checks a "stopping" flag and returns early.

This is a regression from .NET Framework 4.8, where BeginRead used the native APM/OverlappedAsyncResult
path with no Task wrapper involved.

Root cause: Socket.BeginReceive wraps ReceiveAsync(...).AsTask() in a TaskAsyncResult via
TaskToAsyncResult.Begin. When the socket is disposed, the inner Task faults. If the callback never
calls End, task.Exception is never accessed, so the exception is unobserved. On GC, the faulted
Task finalizer raises UnobservedTaskException.

Fix: In TaskAsyncResult, access task.Exception (which calls MarkExceptionObserved() internally)
before invoking the callback, in both the synchronous-completion and async-continuation paths. End still
re-throws the exception correctly if called — observing does not suppress.

This is the same pattern used in #114226 for ResettableValueTaskSource in QUIC.

Changes

  • TaskToAsyncResult.cs: Observe task.Exception before invoking the callback in both paths.
  • TaskToAsyncResultTests.cs: Two regression tests covering async and sync completion paths.

Fixes #126148
Reference: #114226

…ips EndXxx

In the APM pattern, exceptions from the underlying operation are meant to
be surfaced exclusively through the End method. However, if the callback
passed to Begin does not call End (a valid shutdown pattern), the wrapped
Task's exception remains unobserved. When the TaskAsyncResult and its
inner Task are garbage-collected, TaskScheduler.UnobservedTaskException
fires — a regression from .NET Framework 4.8, which had no Task wrapper.

Fix: access task.Exception (marking it observed) before invoking the
callback in both the synchronous-completion path and the async continuation
path. End still re-throws the exception correctly if it is called.

This matches the pattern used in PR dotnet#114226 for ResettableValueTaskSource.

Fixes dotnet#126148
@dotnet-policy-service dotnet-policy-service bot added the community-contribution Indicates that the PR has been added by a community member label Mar 26, 2026
@dotnet-policy-service
Copy link
Contributor

Tagging subscribers to this area: @dotnet/area-system-threading-tasks
See info in area-owners.md if you want to be subscribed.

@mad1081
Copy link
Author

mad1081 commented Mar 26, 2026

@dotnet-policy-service agree

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-System.Threading.Tasks community-contribution Indicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

NetworkStream.BeginRead produces UnobservedTaskException on socket disposal (.NET 10)

1 participant