5

Compiler warning CS4014 (calling async method without awaiting result) is not emitted as a warning during build when the called method is in a referenced assembly.

When the called method is in the same assembly the warning is correctly emitted.

The compiler warning is signaled in Visual Studio when both projects are contained in the same solution.

The difference seems to be caused by the compiler having only the compiled referenced assembly and Visual Studio having the source code to both assemblies.

The question is: why have these two different behaviors? And is there any way to have the CS4014 warning emitted during compilation?

To replicate this behavior setup two class libraries, both having one code file:

TestClassLibrary1

public class Class1
{
    public static async Task<string> DoSomething()
    {
        return await Task.FromResult("test");
    }
}

TestClassLibrary2 (referencing TestClassLibrary1)

public class Class2
{
    public void CallingDoSomething()
    {
        Class1.DoSomething();
    }
}

Compiling these projects will complete without warnings. Opening them in the same solution in Visual Studio will result in 1 error being shown in the Error List and a red squiggly line under Class1.DoSomething().

Benjamin Wegman
  • 465
  • 4
  • 15

1 Answers1

6

The async modifier allows you to write code that returns a Task more conveniently (with await), but it has no representation in IL*. In a compiled assembly, the method simply looks like public static Task<string> DoSomething to the compiler, and calling those without awaiting their result doesn't trigger the warning (even if the methods live in the same assembly). Replace Class1.DoSomething with something else that returns a task and ought to be awaited (say Task.Delay(2000)) and you'll likewise see the compiler doesn't warn. When all of the source code is available, however, the compiler (and by compiler I mean Roslyn) can identify the method as async because the modifier is still part of the syntax tree.

So why doesn't the compiler just always warn when you call a method returning a Task without using the result, regardless of whether it happened to be written using async? Good question. While there are plenty of legitimate scenarios where you don't wait to await a Task (because you want to pass it to Task.WhenAll for example) all of these involve storing the Task somewhere else, which would not raise the warning. Calling a method that returns a Task and discarding the result entirely is almost certainly a mistake (and when it's intentional, there are elegant ways of suppressing the warning), which is why this warning exists in the first place.

I suspect the implementation of this warning could use a tweak (or a replacement with a new warning), but only the people working on the compiler would know that for sure.


*: this isn't actually true; async methods have an AsyncStateMachineAttribute applied to them for more convenient debugging. But, for whatever reason, the compiler doesn't use this to identify async methods across assemblies. Nor should it, arguably: there's nothing particularly special about async in terms of the Task the method returns. But if they wanted to preserve the stated semantics of CS4104 exactly (warn if the result of an async method is unused) this would be one way to do it.

Community
  • 1
  • 1
Jeroen Mostert
  • 27,176
  • 2
  • 52
  • 85
  • Perhaps the prevalence of `Task.Run()` for launching fire-and-forget tasks means that the false positive rate would be too high to make the warning unconditional. – Damien_The_Unbeliever Oct 06 '16 at 12:28
  • Great answer, thanks! I have been working with a library called MassTransit that uses a pattern where it stores a Task internally and returns the same task. The caller has two options: 1. await the returned Task to make sure the operation completes correctly or 2. let MassTransit await the Task and handle any exceptions. So there might be legitimate uses of calling methods without awaiting. Would raising an issue on the Roslyn GitHub repo help? – Benjamin Wegman Oct 10 '16 at 07:59
  • @BenjaminWegman: that usage pattern might be better served by offering two methods, one that returns the task and one that doesn't. But if you're stuck with that interface, a `.Forget()` extension method that discards the task deliberately as in the linked answer might be worthwhile. As for raising an issue on Roslyn, I'm not involved in the project at all so I don't know the bug culture. In that sense I'm not qualified to make any statement what "helps". – Jeroen Mostert Oct 10 '16 at 09:54