runtime: Channels can easily have unobserved exceptions

Description

A user reported an issue where there was an UnobservedTaskException in code that used a Channel. The problem was that the Exception was observed via Channel.Reader.WaitToReadAsync(), but the same exception is also set on a TaskCompletionSource that is visible via Channel.Reader.Completion. So proper code needs to know that it has to observe the exception twice to avoid an UnobservedTaskException.

Reproduction Steps

Original code that was problematic.

TaskScheduler.UnobservedTaskException += (sender, ex) =>
{
    Console.WriteLine(ex.Exception);
};
await Func();

GC.Collect();

Console.ReadKey();

static async Task Func()
{
    var channel = Channel.CreateBounded<int>(2);
    channel.Writer.Complete(new Exception());

    try
    {
        while (await channel.Reader.WaitToReadAsync().ConfigureAwait(false)) // throws
        {
            while (channel.Reader.TryRead(out var item))
            {
            }
        }

        // Observe any errors in the completion task
        await channel.Reader.Completion;
        // We tried to be good citizens and observe the Task, but even this seems like a stretch for consumers to know that they should do
    }
    catch { }
}

The fix would be to move await channel.Reader.Completion; into a finally, and to wrap it in another try catch.

Expected behavior

Expected no UnobservedTaskException if we observed the exception from one of the read APIs.

At a bare minimum the Channel docs should show using the Reader.Completion property properly.

One suggestion was to make Reader.Completion lazy, it looks like that might be possible since we set the _parent._doneWriting exception property everywhere that we complete the Reader.Completion TCS currently. This would make it so you don’t need to observe the Reader.Completion task unless you already grabbed the property.

Actual behavior

UnobservedTaskException due to not observing Reader.Completion.

Regression?

No.

Known Workarounds

Double observe the exception by moving await Reader.Completion into a finally block.

Configuration

No response

Other information

No response

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Reactions: 2
  • Comments: 28 (27 by maintainers)

Most upvoted comments

https://learn.microsoft.com/en-us/dotnet/api/system.windows.application.dispatcherunhandledexception?view=netframework-4.8.1#remarks

By default, Windows Presentation Foundation catches unhandled exceptions, notifies users of the exception from a dialog box (from which they can report the exception), and automatically shuts down an application.

That’s “unhandled”, not “unobserved”. UnobservedTaskException doesn’t show up anywhere in dotnet/wpf. https://github.com/dotnet/wpf/search?q=UnobservedTaskException

The original scenario, when you never read the Completion task, but you use ReadAllAsync or ReadAsync or WaitToReadAsync. If the exception is observed in a reading loop with one of those APIs, there shouldn’t be an unobserved task exception.

Surprisingly the TPL Dataflow has different behavior though.

That’s because it happens to access the Task’s Exception as an implementation detail. Change your example to ActionBlock, for example, and I expect you will see an exception.