113
votes

I have a interface which exposes some async methods. More specifically it has methods defined which return either Task or Task<T>. I am using the async/await keywords.

I am in the process of implementing this interface. However, in some of these methods this implementation doesn't have anything to await. For that reason I am getting the compiler warning "CS1998: This async method lacks 'await' operators and will run synchronously..."

I understand why I am getting these warnings but am wondering whether I should do anything about them in this context. It feels wrong to ignore compiler warnings.

I know I can fix it by awaiting on Task.Run but that feels wrong for a method that is only doing a few inexpensive operations. It also sounds like it will add unneeded overhead to the execution but then I am also not sure if that is already there because the async keyword is present.

Should I just ignore the warnings or is there a way of working around this that I am not seeing?

6
It's going to depend on the specifics. Are you really sure you want these operations to be performed synchronously? If you do want them to be performed synchronously, why is the method marked as async?Servy
Just remove the async keyword. You can still return a Task using Task.FromResult.Michael Liu
@BenVoigt Google is full of information about it, in the event that the OP doesn't already know.Servy
@BenVoigt Didn't Michael Liu already provide that hint? Use Task.FromResult.user743382
@hvd: That was edited into his comment later.Ben Voigt

6 Answers

163
votes

The async keyword is merely an implementation detail of a method; it isn't part of the method signature. If one particular method implementation or override has nothing to await, then just omit the async keyword and return a completed task using Task.FromResult<TResult>:

public Task<string> Foo()               //    public async Task<string> Foo()
{                                       //    {
    Baz();                              //        Baz();
    return Task.FromResult("Hello");    //        return "Hello";
}                                       //    }

If your method returns Task instead of Task<TResult>, then you can return a completed task of any type and value. Task.FromResult(0) seems to be a popular choice:

public Task Bar()                       //    public async Task Bar()
{                                       //    {
    Baz();                              //        Baz();
    return Task.FromResult(0);          //
}                                       //    }

Or, as of .NET Framework 4.6, you can return Task.CompletedTask:

public Task Bar()                       //    public async Task Bar()
{                                       //    {
    Baz();                              //        Baz();
    return Task.CompletedTask;          //
}                                       //    }
19
votes

It's perfectly reasonable that some "asynchronous" operations complete synchronously, yet still conform to the asynchronous call model for the sake of polymorphism.

A real-world example of this is with the OS I/O APIs. Asynchronous and overlapped calls on some devices always complete inline (writing to a pipe implemented using shared memory, for example). But they implement the same interface as multi-part operations which do continue in the background.

4
votes

Michael Liu answered well your question about how you can avoid the warning: by returning Task.FromResult.

I'm going to answer the "Should I worry about the warning" part of your question.

The answer is Yes!

The reason for this is that the warning frequently results when you call a method that returns Task inside of an async method without the await operator. I just fixed a concurrency bug that happened because I invoked an operation in Entity Framework without awaiting the previous operation.

If you can meticulously write your code to avoid compiler warnings, then when there is a warning, it will stand out like a sore thumb. I could have avoided several hours of debugging.

4
votes

It might be too late but it might be useful investigation:

There is about inner structure of compiled code (IL):

 public static async Task<int> GetTestData()
    {
        return 12;
    }

it becomes to in IL:

.method private hidebysig static class [mscorlib]System.Threading.Tasks.Task`1<int32> 
        GetTestData() cil managed
{
  .custom instance void [mscorlib]System.Runtime.CompilerServices.AsyncStateMachineAttribute::.ctor(class [mscorlib]System.Type) = ( 01 00 28 55 73 61 67 65 4C 69 62 72 61 72 79 2E   // ..(UsageLibrary.
                                                                                                                                     53 74 61 72 74 54 79 70 65 2B 3C 47 65 74 54 65   // StartType+<GetTe
                                                                                                                                     73 74 44 61 74 61 3E 64 5F 5F 31 00 00 )          // stData>d__1..
  .custom instance void [mscorlib]System.Diagnostics.DebuggerStepThroughAttribute::.ctor() = ( 01 00 00 00 ) 
  // Code size       52 (0x34)
  .maxstack  2
  .locals init ([0] class UsageLibrary.StartType/'<GetTestData>d__1' V_0,
           [1] valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> V_1)
  IL_0000:  newobj     instance void UsageLibrary.StartType/'<GetTestData>d__1'::.ctor()
  IL_0005:  stloc.0
  IL_0006:  ldloc.0
  IL_0007:  call       valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<!0> valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::Create()
  IL_000c:  stfld      valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> UsageLibrary.StartType/'<GetTestData>d__1'::'<>t__builder'
  IL_0011:  ldloc.0
  IL_0012:  ldc.i4.m1
  IL_0013:  stfld      int32 UsageLibrary.StartType/'<GetTestData>d__1'::'<>1__state'
  IL_0018:  ldloc.0
  IL_0019:  ldfld      valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> UsageLibrary.StartType/'<GetTestData>d__1'::'<>t__builder'
  IL_001e:  stloc.1
  IL_001f:  ldloca.s   V_1
  IL_0021:  ldloca.s   V_0
  IL_0023:  call       instance void valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::Start<class UsageLibrary.StartType/'<GetTestData>d__1'>(!!0&)
  IL_0028:  ldloc.0
  IL_0029:  ldflda     valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> UsageLibrary.StartType/'<GetTestData>d__1'::'<>t__builder'
  IL_002e:  call       instance class [mscorlib]System.Threading.Tasks.Task`1<!0> valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::get_Task()
  IL_0033:  ret
} // end of method StartType::GetTestData

And without async and task method:

 public static int GetTestData()
        {
            return 12;
        }

becomes :

.method private hidebysig static int32  GetTestData() cil managed
{
  // Code size       8 (0x8)
  .maxstack  1
  .locals init ([0] int32 V_0)
  IL_0000:  nop
  IL_0001:  ldc.i4.s   12
  IL_0003:  stloc.0
  IL_0004:  br.s       IL_0006
  IL_0006:  ldloc.0
  IL_0007:  ret
} // end of method StartType::GetTestData

As you could see the big difference between these methods. If you don't use await inside async method and do not care about using of async method (for example API call or event handler) the good idea will convert it to normal sync method (it saves your application performance).

Updated:

There is also additional information from microsoft docs:

async methods need to have an await keyword in their body or they will never yield! This is important to keep in mind. If await is not used in the body of an async method, the C# compiler will generate a warning, but the code will compile and run as if it were a normal method. Note that this would also be incredibly inefficient, as the state machine generated by the C# compiler for the async method would not be accomplishing anything.

2
votes

Note on exception behaviour when returning Task.FromResult

Here's a little demo which shows the difference in exception handling between methods marked and not marked with async.

public Task<string> GetToken1WithoutAsync() => throw new Exception("Ex1!");

// Warning: This async method lacks 'await' operators and will run synchronously. Consider ...
public async Task<string> GetToken2WithAsync() => throw new Exception("Ex2!");  

public string GetToken3Throws() => throw new Exception("Ex3!");
public async Task<string> GetToken3WithAsync() => await Task.Run(GetToken3Throws);

public async Task<string> GetToken4WithAsync() { throw new Exception("Ex4!"); return await Task.FromResult("X");} 


public static async Task Main(string[] args)
{
    var p = new Program();

    try { var task1 = p.GetToken1WithoutAsync(); } 
    catch( Exception ) { Console.WriteLine("Throws before await.");};

    var task2 = p.GetToken2WithAsync(); // Does not throw;
    try { var token2 = await task2; } 
    catch( Exception ) { Console.WriteLine("Throws on await.");};

    var task3 = p.GetToken3WithAsync(); // Does not throw;
    try { var token3 = await task3; } 
    catch( Exception ) { Console.WriteLine("Throws on await.");};

    var task4 = p.GetToken4WithAsync(); // Does not throw;
    try { var token4 = await task4; } 
    catch( Exception ) { Console.WriteLine("Throws on await.");};
}
// .NETCoreApp,Version=v3.0
Throws before await.
Throws on await.
Throws on await.
Throws on await.

(Cross post of my answer for When async Task<T> required by interface, how to get return variable without compiler warning)

2
votes

Only if you are actually calling the method involved, and only if performance is a concern.

This can be demonstrated by writing a program containing the following 4 methods, then decompiling them to IL (note that IL presented may change between runtime versions; the below is from .NET Core 3.1):

int FullySync() => 42;

Task<int> TaskSync() => Task.FromResult(42);

// CS1998
async Task<int> NotActuallyAsync() => 42;

async Task<int> FullyAsync() => await Task.Run(() => 42);

The first two result in very short method bodies containing exactly what you would expect:

.method private hidebysig 
    instance int32 FullySync () cil managed 
{
    // Method begins at RVA 0x205e
    // Code size 3 (0x3)
    .maxstack 8

    // return 42;
    IL_0000: ldc.i4.s 42
    IL_0002: ret
} // end of method Program::FullySync

.method private hidebysig 
    instance class [System.Runtime]System.Threading.Tasks.Task`1<int32> TaskSync () cil managed 
{
    // Method begins at RVA 0x2062
    // Code size 8 (0x8)
    .maxstack 8

    // return Task.FromResult(42);
    IL_0000: ldc.i4.s 42
    IL_0002: call class [System.Runtime]System.Threading.Tasks.Task`1<!!0> [System.Runtime]System.Threading.Tasks.Task::FromResult<int32>(!!0)
    IL_0007: ret
} // end of method Program::TaskSync

But the presence of the async keyword on the last two causes the compiler to generate asynchronous state machines for those methods:

.method private hidebysig 
    instance class [System.Runtime]System.Threading.Tasks.Task`1<int32> NotActuallyAsync () cil managed 
{
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.AsyncStateMachineAttribute::.ctor(class [System.Runtime]System.Type) = (
        01 00 29 43 53 31 39 39 38 54 65 73 74 2e 50 72
        6f 67 72 61 6d 2b 3c 4e 6f 74 41 63 74 75 61 6c
        6c 79 41 73 79 6e 63 3e 64 5f 5f 33 00 00
    )
    .custom instance void [System.Diagnostics.Debug]System.Diagnostics.DebuggerStepThroughAttribute::.ctor() = (
        01 00 00 00
    )
    // Method begins at RVA 0x206c
    // Code size 56 (0x38)
    .maxstack 2
    .locals init (
        [0] class CS1998Test.Program/'<NotActuallyAsync>d__3'
    )

    IL_0000: newobj instance void CS1998Test.Program/'<NotActuallyAsync>d__3'::.ctor()
    IL_0005: stloc.0
    IL_0006: ldloc.0
    IL_0007: call valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<!0> valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::Create()
    IL_000c: stfld valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> CS1998Test.Program/'<NotActuallyAsync>d__3'::'<>t__builder'
    IL_0011: ldloc.0
    IL_0012: ldarg.0
    IL_0013: stfld class CS1998Test.Program CS1998Test.Program/'<NotActuallyAsync>d__3'::'<>4__this'
    IL_0018: ldloc.0
    IL_0019: ldc.i4.m1
    IL_001a: stfld int32 CS1998Test.Program/'<NotActuallyAsync>d__3'::'<>1__state'
    IL_001f: ldloc.0
    IL_0020: ldflda valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> CS1998Test.Program/'<NotActuallyAsync>d__3'::'<>t__builder'
    IL_0025: ldloca.s 0
    IL_0027: call instance void valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::Start<class CS1998Test.Program/'<NotActuallyAsync>d__3'>(!!0&)
    IL_002c: ldloc.0
    IL_002d: ldflda valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> CS1998Test.Program/'<NotActuallyAsync>d__3'::'<>t__builder'
    IL_0032: call instance class [System.Runtime]System.Threading.Tasks.Task`1<!0> valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::get_Task()
    IL_0037: ret
} // end of method Program::NotActuallyAsync

.class nested private auto ansi sealed beforefieldinit '<NotActuallyAsync>d__3'
    extends [System.Runtime]System.Object
    implements [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine
{
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
        01 00 00 00
    )
    // Fields
    .field public int32 '<>1__state'
    .field public valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> '<>t__builder'
    .field public class CS1998Test.Program '<>4__this'

    // Methods
    .method public hidebysig specialname rtspecialname 
        instance void .ctor () cil managed 
    {
        // Method begins at RVA 0x20fd
        // Code size 8 (0x8)
        .maxstack 8

        // {
        IL_0000: ldarg.0
        // (no C# code)
        IL_0001: call instance void [System.Runtime]System.Object::.ctor()
        // }
        IL_0006: nop
        IL_0007: ret
    } // end of method '<NotActuallyAsync>d__3'::.ctor

    .method private final hidebysig newslot virtual 
        instance void MoveNext () cil managed 
    {
        .override method instance void [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine::MoveNext()
        // Method begins at RVA 0x2108
        // Code size 58 (0x3a)
        .maxstack 2
        .locals init (
            [0] int32,
            [1] int32,
            [2] class [System.Runtime]System.Exception
        )

        // int num = <>1__state;
        IL_0000: ldarg.0
        IL_0001: ldfld int32 CS1998Test.Program/'<NotActuallyAsync>d__3'::'<>1__state'
        IL_0006: stloc.0
        .try
        {
            // result = 42;
            IL_0007: ldc.i4.s 42
            IL_0009: stloc.1
            // }
            IL_000a: leave.s IL_0024
        } // end .try
        catch [System.Runtime]System.Exception
        {
            // catch (Exception exception)
            IL_000c: stloc.2
            // <>1__state = -2;
            IL_000d: ldarg.0
            IL_000e: ldc.i4.s -2
            IL_0010: stfld int32 CS1998Test.Program/'<NotActuallyAsync>d__3'::'<>1__state'
            // <>t__builder.SetException(exception);
            IL_0015: ldarg.0
            IL_0016: ldflda valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> CS1998Test.Program/'<NotActuallyAsync>d__3'::'<>t__builder'
            IL_001b: ldloc.2
            IL_001c: call instance void valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::SetException(class [System.Runtime]System.Exception)
            // return;
            IL_0021: nop
            IL_0022: leave.s IL_0039
        } // end handler

        // <>1__state = -2;
        IL_0024: ldarg.0
        IL_0025: ldc.i4.s -2
        IL_0027: stfld int32 CS1998Test.Program/'<NotActuallyAsync>d__3'::'<>1__state'
        // <>t__builder.SetResult(result);
        IL_002c: ldarg.0
        IL_002d: ldflda valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> CS1998Test.Program/'<NotActuallyAsync>d__3'::'<>t__builder'
        IL_0032: ldloc.1
        IL_0033: call instance void valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::SetResult(!0)
        // }
        IL_0038: nop

        IL_0039: ret
    } // end of method '<NotActuallyAsync>d__3'::MoveNext

    .method private final hidebysig newslot virtual 
        instance void SetStateMachine (
            class [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine stateMachine
        ) cil managed 
    {
        .custom instance void [System.Diagnostics.Debug]System.Diagnostics.DebuggerHiddenAttribute::.ctor() = (
            01 00 00 00
        )
        .override method instance void [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine::SetStateMachine(class [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine)
        // Method begins at RVA 0x2160
        // Code size 1 (0x1)
        .maxstack 8

        // }
        IL_0000: ret
    } // end of method '<NotActuallyAsync>d__3'::SetStateMachine

} // end of class <NotActuallyAsync>d__3

.method private hidebysig 
    instance class [System.Runtime]System.Threading.Tasks.Task`1<int32> FullyAsync () cil managed 
{
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.AsyncStateMachineAttribute::.ctor(class [System.Runtime]System.Type) = (
        01 00 23 43 53 31 39 39 38 54 65 73 74 2e 50 72
        6f 67 72 61 6d 2b 3c 46 75 6c 6c 79 41 73 79 6e
        63 3e 64 5f 5f 34 00 00
    )
    .custom instance void [System.Diagnostics.Debug]System.Diagnostics.DebuggerStepThroughAttribute::.ctor() = (
        01 00 00 00
    )
    // Method begins at RVA 0x20b0
    // Code size 56 (0x38)
    .maxstack 2
    .locals init (
        [0] class CS1998Test.Program/'<FullyAsync>d__4'
    )

    IL_0000: newobj instance void CS1998Test.Program/'<FullyAsync>d__4'::.ctor()
    IL_0005: stloc.0
    IL_0006: ldloc.0
    IL_0007: call valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<!0> valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::Create()
    IL_000c: stfld valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> CS1998Test.Program/'<FullyAsync>d__4'::'<>t__builder'
    IL_0011: ldloc.0
    IL_0012: ldarg.0
    IL_0013: stfld class CS1998Test.Program CS1998Test.Program/'<FullyAsync>d__4'::'<>4__this'
    IL_0018: ldloc.0
    IL_0019: ldc.i4.m1
    IL_001a: stfld int32 CS1998Test.Program/'<FullyAsync>d__4'::'<>1__state'
    IL_001f: ldloc.0
    IL_0020: ldflda valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> CS1998Test.Program/'<FullyAsync>d__4'::'<>t__builder'
    IL_0025: ldloca.s 0
    IL_0027: call instance void valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::Start<class CS1998Test.Program/'<FullyAsync>d__4'>(!!0&)
    IL_002c: ldloc.0
    IL_002d: ldflda valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> CS1998Test.Program/'<FullyAsync>d__4'::'<>t__builder'
    IL_0032: call instance class [System.Runtime]System.Threading.Tasks.Task`1<!0> valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::get_Task()
    IL_0037: ret
} // end of method Program::FullyAsync

.class nested private auto ansi sealed beforefieldinit '<FullyAsync>d__4'
    extends [System.Runtime]System.Object
    implements [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine
{
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
        01 00 00 00
    )
    // Fields
    .field public int32 '<>1__state'
    .field public valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> '<>t__builder'
    .field public class CS1998Test.Program '<>4__this'
    .field private int32 '<>s__1'
    .field private valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter`1<int32> '<>u__1'

    // Methods
    .method public hidebysig specialname rtspecialname 
        instance void .ctor () cil managed 
    {
        // Method begins at RVA 0x217b
        // Code size 8 (0x8)
        .maxstack 8

        // {
        IL_0000: ldarg.0
        // (no C# code)
        IL_0001: call instance void [System.Runtime]System.Object::.ctor()
        // }
        IL_0006: nop
        IL_0007: ret
    } // end of method '<FullyAsync>d__4'::.ctor

    .method private final hidebysig newslot virtual 
        instance void MoveNext () cil managed 
    {
        .override method instance void [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine::MoveNext()
        // Method begins at RVA 0x2184
        // Code size 199 (0xc7)
        .maxstack 3
        .locals init (
            [0] int32,
            [1] int32,
            [2] valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter`1<int32>,
            [3] class CS1998Test.Program/'<FullyAsync>d__4',
            [4] class [System.Runtime]System.Exception
        )

        // int num = <>1__state;
        IL_0000: ldarg.0
        IL_0001: ldfld int32 CS1998Test.Program/'<FullyAsync>d__4'::'<>1__state'
        IL_0006: stloc.0
        .try
        {
            // if (num != 0)
            IL_0007: ldloc.0
            IL_0008: brfalse.s IL_000c

            // (no C# code)
            IL_000a: br.s IL_000e

            // awaiter = Task.Run(() => 42).GetAwaiter();
            IL_000c: br.s IL_0065

            IL_000e: ldsfld class [System.Runtime]System.Func`1<int32> CS1998Test.Program/'<>c'::'<>9__4_0'
            IL_0013: dup
            IL_0014: brtrue.s IL_002d

            // (no C# code)
            IL_0016: pop
            // if (!awaiter.IsCompleted)
            IL_0017: ldsfld class CS1998Test.Program/'<>c' CS1998Test.Program/'<>c'::'<>9'
            IL_001c: ldftn instance int32 CS1998Test.Program/'<>c'::'<FullyAsync>b__4_0'()
            IL_0022: newobj instance void class [System.Runtime]System.Func`1<int32>::.ctor(object, native int)
            IL_0027: dup
            IL_0028: stsfld class [System.Runtime]System.Func`1<int32> CS1998Test.Program/'<>c'::'<>9__4_0'

            IL_002d: call class [System.Runtime]System.Threading.Tasks.Task`1<!!0> [System.Runtime]System.Threading.Tasks.Task::Run<int32>(class [System.Runtime]System.Func`1<!!0>)
            IL_0032: callvirt instance valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter`1<!0> class [System.Runtime]System.Threading.Tasks.Task`1<int32>::GetAwaiter()
            IL_0037: stloc.2
            IL_0038: ldloca.s 2
            IL_003a: call instance bool valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter`1<int32>::get_IsCompleted()
            IL_003f: brtrue.s IL_0081

            // num = (<>1__state = 0);
            IL_0041: ldarg.0
            IL_0042: ldc.i4.0
            IL_0043: dup
            IL_0044: stloc.0
            IL_0045: stfld int32 CS1998Test.Program/'<FullyAsync>d__4'::'<>1__state'
            // <>u__1 = awaiter;
            IL_004a: ldarg.0
            IL_004b: ldloc.2
            IL_004c: stfld valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter`1<int32> CS1998Test.Program/'<FullyAsync>d__4'::'<>u__1'
            // <FullyAsync>d__4 stateMachine = this;
            IL_0051: ldarg.0
            IL_0052: stloc.3
            // <>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
            IL_0053: ldarg.0
            IL_0054: ldflda valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> CS1998Test.Program/'<FullyAsync>d__4'::'<>t__builder'
            IL_0059: ldloca.s 2
            IL_005b: ldloca.s 3
            IL_005d: call instance void valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::AwaitUnsafeOnCompleted<valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter`1<int32>, class CS1998Test.Program/'<FullyAsync>d__4'>(!!0&, !!1&)
            // return;
            IL_0062: nop
            IL_0063: leave.s IL_00c6

            // awaiter = <>u__1;
            IL_0065: ldarg.0
            IL_0066: ldfld valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter`1<int32> CS1998Test.Program/'<FullyAsync>d__4'::'<>u__1'
            IL_006b: stloc.2
            // <>u__1 = default(TaskAwaiter<int>);
            IL_006c: ldarg.0
            IL_006d: ldflda valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter`1<int32> CS1998Test.Program/'<FullyAsync>d__4'::'<>u__1'
            IL_0072: initobj valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter`1<int32>
            // num = (<>1__state = -1);
            IL_0078: ldarg.0
            IL_0079: ldc.i4.m1
            IL_007a: dup
            IL_007b: stloc.0
            IL_007c: stfld int32 CS1998Test.Program/'<FullyAsync>d__4'::'<>1__state'

            // <>s__1 = awaiter.GetResult();
            IL_0081: ldarg.0
            IL_0082: ldloca.s 2
            IL_0084: call instance !0 valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter`1<int32>::GetResult()
            IL_0089: stfld int32 CS1998Test.Program/'<FullyAsync>d__4'::'<>s__1'
            // result = <>s__1;
            IL_008e: ldarg.0
            IL_008f: ldfld int32 CS1998Test.Program/'<FullyAsync>d__4'::'<>s__1'
            IL_0094: stloc.1
            // }
            IL_0095: leave.s IL_00b1
        } // end .try
        catch [System.Runtime]System.Exception
        {
            // catch (Exception exception)
            IL_0097: stloc.s 4
            // <>1__state = -2;
            IL_0099: ldarg.0
            IL_009a: ldc.i4.s -2
            IL_009c: stfld int32 CS1998Test.Program/'<FullyAsync>d__4'::'<>1__state'
            // <>t__builder.SetException(exception);
            IL_00a1: ldarg.0
            IL_00a2: ldflda valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> CS1998Test.Program/'<FullyAsync>d__4'::'<>t__builder'
            IL_00a7: ldloc.s 4
            IL_00a9: call instance void valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::SetException(class [System.Runtime]System.Exception)
            // return;
            IL_00ae: nop
            IL_00af: leave.s IL_00c6
        } // end handler

        // <>1__state = -2;
        IL_00b1: ldarg.0
        IL_00b2: ldc.i4.s -2
        IL_00b4: stfld int32 CS1998Test.Program/'<FullyAsync>d__4'::'<>1__state'
        // <>t__builder.SetResult(result);
        IL_00b9: ldarg.0
        IL_00ba: ldflda valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> CS1998Test.Program/'<FullyAsync>d__4'::'<>t__builder'
        IL_00bf: ldloc.1
        IL_00c0: call instance void valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::SetResult(!0)
        // }
        IL_00c5: nop

        IL_00c6: ret
    } // end of method '<FullyAsync>d__4'::MoveNext

    .method private final hidebysig newslot virtual 
        instance void SetStateMachine (
            class [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine stateMachine
        ) cil managed 
    {
        .custom instance void [System.Diagnostics.Debug]System.Diagnostics.DebuggerHiddenAttribute::.ctor() = (
            01 00 00 00
        )
        .override method instance void [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine::SetStateMachine(class [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine)
        // Method begins at RVA 0x2268
        // Code size 1 (0x1)
        .maxstack 8

        // }
        IL_0000: ret
    } // end of method '<FullyAsync>d__4'::SetStateMachine

} // end of class <FullyAsync>d__4

In brief, executing a method marked with the async modifier entails the construction and execution of an asynchronous state machine for that method, regardless of whether that method is actually performing any asynchronous work! As I'm sure you can guess, that entails a performance penalty compared to a standard non-async method, which - depending on your use-case - may or may not be significant.

But this isn't what the CS1998 warning says at all. This warning is intended for the case where you've defined an async method because you need to await something in it, but have simply forgotten to add the await keyword before the asynchronous call.

Your case is essentially the opposite: you've defined a method as async but you know and intend that it doesn't perform any such work. But the compiler has no way of knowing that - to the compiler it looks exactly the same as the previous case, so you get the same warning.

To be honest, in that second case you yourself have caused the warning by unnecessarily adding the async keyword to the implementation. You know that the method isn't doing any asynchronous work, so why bother adding the keyword? You're just bloating it for no good reason.

The warning could certainly be improved to call out the fact that you're basically being silly, and I've opened an issue in the Roslyn repo to hopefully get that done.