Runtime: Attempt to improve no unobserved task exception being raised when trying to await null task

Created on 21 Apr 2017  路  5Comments  路  Source: dotnet/runtime

My application stopped working after targetting netcoreapp2.0 while working perfectly fine in netcoreapp1.1. While the root cause for that is for another issue, it generated quite interesting scenario that I decided to extract.

Consider following reproducable case:

```c#
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;

namespace test {
internal static class Program {
private static readonly ManualResetEventSlim ShutdownEvent = new ManualResetEventSlim(false);

            private static void Main() {
                    AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
                    TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
                    Console.WriteLine("Start");
                    Reproduce().Forget();
                    Console.WriteLine("Forget");
                    ShutdownEvent.Wait();
                    Console.WriteLine("End");
            }

            private static async Task Reproduce() {
                    Console.WriteLine("Start inner");
                    await GetNullTask().ConfigureAwait(false);
                    Console.WriteLine("Set event");
                    ShutdownEvent.Set();
                    Console.WriteLine("End inner");
            }

            private static Task GetNullTask() { return null; }

            private static void Forget(this object obj) { }

            private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs e) {
                    Console.WriteLine("OnUnhandledException!");
            }

            private static void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) {
                    Console.WriteLine("OnUnobservedTaskException!");
            }
    }

}


As you can see, `await GetNullTask().ConfigureAwait(false);` is trying to await a `Task` that is null. Thing is, nothing is happening, the function is entirely frozen - no unhandled exception being raised, no unobserved task exception being raised (which should happen), and no function returning - limbo.

root@debian:~/test/test# dotnet exec bin/test.dll
Start
Start inner
Forget


I'm pretty sure this is **really** unwanted - I didn't test if the same behaviour exists in .NET core 1.1, but regardless if it does, I think this should be corrected with proper (unobserved task) exception being raised. Otherwise there is no way to even guess that some problem occured, and unobserved task exception event is the function that should catch situation like that.

Of course there is no issue if we replace `Reproduce().Forget()` with `Reproduce.Wait()`:

Unhandled Exception: System.AggregateException: One or more errors occurred. (Object reference not set to an instance of an object.) ---> System.NullReferenceException: Object reference not set to an instance of an object.
at test.Program.d__2.MoveNext()
--- End of inner exception stack trace ---
at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)
at System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken)
at System.Threading.Tasks.Task.Wait()
at test.Program.Main()


But in `Forget()` case, there should still be `TaskScheduler.UnobservedTaskException` being raised.

.NET Command Line Tools (2.0.0-preview1-005825)

Product Information:
Version: 2.0.0-preview1-005825
Commit SHA-1 hash: b4a821109d

Runtime Environment:
OS Name: debian
OS Version: 9
OS Platform: Linux
RID: ubuntu.16.10-x64
Base Path: /opt/dotnet/sdk/2.0.0-preview1-005825/

Microsoft .NET Core Shared Framework Host

Version : 2.0.0-preview1-002028-00
Build : 6d0caecf154d3398b9bc0e637089b6af9e250479
```

area-System.Threading.Tasks question

Most helpful comment

In this case I'm wondering if there is any chance to do something in such case, as if there are no allocations, GC won't kick in and won't handle the exception. Usually this is not a problem as there are more things going on, but in this specific case there is no way to know what happened, as even during debugging it might not be obvious where is the issue, GetNullTask() could be far more complex function that does return null somewhere. Personally in real-world scenario I triggered it by await someObject.DoAsync(), where someObject was null. This is far more common scenario, the above one was only for reproducing the problem.

As this is not a bug, I'm fine with closing the issue, but I'd be happy if maybe we could in some way improve it, as the only thing possible to do right now is launching another task that will periodically call GC.Collect();, and as you noticed this is not really the best way to debug the issue, and I believe that there should be some improvement over current situation.

Wouldn't it be possible to add some extra code that would handle faulty tasks and propagate exception immediately instead of handling responsibility to the GC? This is only an idea, I'm sure you could think of something better that would improve programming experience in such case.

All 5 comments

I don't see any issue here, or any change between 1.x and 2.0.

UnobservedTaskException is raised when a Task with an unobserved exception is finalized. Since you don't trigger GC after the Task is forgotten, it doesn't get finalized and so UnobservedTaskException is never raised. This has been the behavior since .Net 4.0.

If you manually trigger GC by adding GC.Collect(); after Reproduce().Forget(); and before ShutdownEvent.Wait();, you will see UnobservedTaskException being raised.

The only thing I can think of that could fix this would be if the CLR triggered GC periodically, even when no allocations are happening. That would be a really bad idea in production code (because it would hurt performance just to help debugging), but maybe it could make sense in debug mode? Though it would make debug mode different from release mode, which might be unwanted. I realized this wouldn't be worth it.

TLDR: I don't think there is any bug or compatibility issue here. But maybe there is a chance to improve the debugging experience.

In this case I'm wondering if there is any chance to do something in such case, as if there are no allocations, GC won't kick in and won't handle the exception. Usually this is not a problem as there are more things going on, but in this specific case there is no way to know what happened, as even during debugging it might not be obvious where is the issue, GetNullTask() could be far more complex function that does return null somewhere. Personally in real-world scenario I triggered it by await someObject.DoAsync(), where someObject was null. This is far more common scenario, the above one was only for reproducing the problem.

As this is not a bug, I'm fine with closing the issue, but I'd be happy if maybe we could in some way improve it, as the only thing possible to do right now is launching another task that will periodically call GC.Collect();, and as you noticed this is not really the best way to debug the issue, and I believe that there should be some improvement over current situation.

Wouldn't it be possible to add some extra code that would handle faulty tasks and propagate exception immediately instead of handling responsibility to the GC? This is only an idea, I'm sure you could think of something better that would improve programming experience in such case.

Wouldn't it be possible to add some extra code that would handle faulty tasks and propagate exception immediately instead of handling responsibility to the GC?

How would that code know whether the exception is unobserved? Or do you want a way to get notified of all faulting Tasks?


About debugging your specific scenario: wouldn't enabling "break when exception is thrown" help you? Especially since that experience got some nice improvements in VS 2017.

How would that code know whether the exception is unobserved?

I thought about some middle-man that would await all tasks that are being forgotten, and immediately after they finish, propagate the exception, instead of waiting for GC to collect them.

Or do you want a way to get notified of all faulting Tasks?

Either is fine, I just want to have a way of detecting such case without waiting for GC to do the job. I'm pretty sure it'd be useful for far more people, as such things might be unobserved otherwise (like in my case, I had to spend some time debugging to find out where is the issue, if I had notification I'd know right away that I have NRE somewhere there instead of believing my app is simply waiting for task to be finished).

Everything here is behaving by design. Tasks need to be able to propagate exceptions to wherever that Task is later awaited, which means the exceptions must be caught and stored, and then it's only when we can be sure that the Task will never have its exception observed that we can raise the unobserved event... and the only way we can know that is when the object becomes collectible from the GC's perspective.

Was this page helpful?
0 / 5 - 0 ratings