Search code examples
c#.netasynchronousasync-awaittask-parallel-library

Task return type with and without Async


I little bit confused on the behavior of the async keyword.

Lets say I have 2 methods,

public async Task DoSomething1()
{
    await Task.Run(() =>
    {
        for(int i = 0; i<3; i++)
        {
            Console.WriteLine("I m doing something:" + i);
        }
    });
}

And

public Task DoSomething2()
{
    return Task.Run(() =>
    {
        for(int i = 0; i<3; i++)
        {
            Console.WriteLine("I m doing something:" + i);
        }
    });
}

From my understanding both methods are awaitable. But when I write a method that has a Task return type without the async keyword I need to return a Task otherwise the compiler generates an error. Its a common thing that a method should return its type. But when I use it with the async keyword the compiler generates another error that you can't return a Task. So what does it mean? I am totally confused here.


Solution

  • General Async Info

    If you use await in your code, you are required to use the async keyword on the method.

    If you use async and want to return an actual type, you can declare that your method returns the type as a generic Task like this Task<int>.

    Here are the valid return types for an async method:

    https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/async-return-types

    1. Task<TResult>, for an async method that returns a value.
    2. Task, for an async method that performs an operation but returns no value.
    3. void, for an event handler.

    A new answer August 2022

    TL;DR - The return type is automatically wrapped in a Task.

    I was receiving downvotes on this so I re-read the question in more detail and got to the root of it. It's not about the return type in the signature of the method. Its about the return value; why do I not need to return an explicit Task type?

    Anyway, let's look at this method:

    public Task TaskMethod() 
    {
        return Task.CompletedTask;        
    }
    

    This seems pretty normal and what we are used to in C#. You declare the return type and return an object of that type. Easy. (In fact the "lowered" code is the exact same.)

    Now for the async case.

    public async Task MethodAsync() 
    {
        return Task.CompletedTask;  
    }
    

    This generates a compile error: error CS1997: Since 'C.MethodAsync()' is an async method that returns 'Task', a return keyword must not be followed by an object expression. Did you intend to return 'Task<T>'?

    OK, then we can't return a task directly, let's do the right thing. It is easier to see this example if we return something so, let's return int.

    public async Task<int> MethodAsync() 
    {
        return 1;
    }
    

    (This method gives a warning that we are using async without await, but ignore this for now.)

    This method is declared to return a Task<int>, but it only returns an int! What gives?

    The lowered code is complicated, but you can see that a state machine will be generated. The MethodAsync looks like this:

    [CompilerGenerated]
    private sealed class <MethodAsync>d__0 : IAsyncStateMachine
    {
        public int <>1__state;
    
        public AsyncTaskMethodBuilder<int> <>t__builder;
    
        public C <>4__this;
    
        private void MoveNext()
        {
            int num = <>1__state;
            int result;
            try
            {
                result = 1;
            }
            catch (Exception exception)
            {
                <>1__state = -2;
                <>t__builder.SetException(exception);
                return;
            }
            <>1__state = -2;
            <>t__builder.SetResult(result);
        }
    
        void IAsyncStateMachine.MoveNext()
        {
            //ILSpy generated this explicit interface implementation from .override directive in MoveNext
            this.MoveNext();
        }
    
        [DebuggerHidden]
        private void SetStateMachine(IAsyncStateMachine stateMachine)
        {
        }
    
        void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
        {
            //ILSpy generated this explicit interface implementation from .override directive in SetStateMachine
            this.SetStateMachine(stateMachine);
        }
    }
    
    [AsyncStateMachine(typeof(<MethodAsync>d__0))]
    [DebuggerStepThrough]
    public Task<int> MethodAsync()
    {
        <MethodAsync>d__0 stateMachine = new <MethodAsync>d__0();
        stateMachine.<>t__builder = AsyncTaskMethodBuilder<int>.Create();
        stateMachine.<>4__this = this;
        stateMachine.<>1__state = -1;
        stateMachine.<>t__builder.Start(ref stateMachine);
        return stateMachine.<>t__builder.Task;
    }
    

    And you can see the Task here is automatically returned in the public method. The state machine implements the method body.

    You can see that the actual value returned is the int from <>t__builder wrapped in a Task.

    To see the lowered code yourself, you can try it in https://sharplab.io.

    Also after writing all this I found another answer that explains it in a different way. Guess who wrote that answer? https://stackoverflow.com/a/37647093/1804678