TX_
TX_

Reputation: 5476

Why use async and return await, when you can return Task<T> directly?

Is there any scenario where writing method like this:

public async Task<SomeResult> DoSomethingAsync()
{
    // Some synchronous code might or might not be here... //
    return await DoAnotherThingAsync();
}

instead of this:

public Task<SomeResult> DoSomethingAsync()
{
    // Some synchronous code might or might not be here... //
    return DoAnotherThingAsync();
}

would make sense?

Why use return await construct when you can directly return Task<T> from the inner DoAnotherThingAsync() invocation?

I see code with return await in so many places, I think I might have missed something. But as far as I understand, not using async/await keywords in this case and directly returning the Task would be functionally equivalent. Why add additional overhead of additional await layer?

Upvotes: 404

Views: 81002

Answers (10)

Oliver
Oliver

Reputation: 45119

This would prevent creating the task state machine that would be created under the hood at compile time. But according to David you should prefer async/await over directly returning Task for these reasons:

  • Asynchronous and synchronous exceptions are normalized to always be asynchronous.
  • The code is easier to modify (consider adding a using, for example).
  • Diagnostics of asynchronous methods are easier (debugging hangs etc).
  • Exceptions thrown will be automatically wrapped in the returned Task instead of surprising the caller with an actual exception.
  • Async locals will not leak out of async methods. If you set an async local in a non-async method, it will "leak" out of that call.

💡NOTE: There are performance considerations when using an async state machine over directly returning the Task. It's always faster to directly return the Task since it does less work but you end up changing the behavior and potentially losing some of the benefits of the async state machine.

If you really want to break out of the asynchrony, you should know that asynchrony is viral:

Once you go async, all of your callers SHOULD be async, since efforts to be async amount to nothing unless the entire callstack is async. In many cases, being partially async can be worse than being entirely synchronous. Therefore it is best to go all in, and make everything async at once.

Upvotes: 3

Luke Vo
Luke Vo

Reputation: 20788

Another problem with non-await method is sometimes you cannot implicitly cast the return type, especially with Task<IEnumerable<T>>:

async Task<List<string>> GetListAsync(string foo) => new();

// This method works
async Task<IEnumerable<string>> GetMyList() => await GetListAsync("myFoo");

// This won't work
Task<IEnumerable<string>> GetMyListNoAsync() => GetListAsync("myFoo");

The error:

Cannot implicitly convert type 'System.Threading.Tasks.Task<System.Collections.Generic.List>' to 'System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable>'

Upvotes: 2

Sedat Kapanoglu
Sedat Kapanoglu

Reputation: 47690

Another reason for why you may want to return await: The await syntax lets you avoid hitting a mismatch between Task<T> and ValueTask<T> types. For example, the code below works even though SubTask method returns Task<T> but its caller returns ValueTask<T>.

async Task<T> SubTask()
{
...
}

async ValueTask<T> DoSomething()
{
  await UnimportantTask();
  return await SubTask();
}

If you skip await on the DoSomething() line, you'll get a compiler error CS0029:

Cannot implicitly convert type 'System.Threading.Tasks.Task<BlaBla>' to 'System.Threading.Tasks.ValueTask<BlaBla>'.

You'll get CS0030 too, if you try to explicitly typecast it.

This is .NET Framework, by the way. I can totally foresee a comment saying "that's fixed in .NET hypothetical_version", I haven't tested it. :)

Upvotes: 2

haimb
haimb

Reputation: 419

If you won't use return await you could ruin your stack trace while debugging or when it's printed in the logs on exceptions.

When you return the task, the method fulfilled its purpose and it's out of the call stack. When you use return await you're leaving it in the call stack.

For example:

Call stack when using await: A awaiting the task from B => B awaiting the task from C

Call stack when not using await: A awaiting the task from C, which B has returned.

Upvotes: 24

Stephen Cleary
Stephen Cleary

Reputation: 457322

If you don't need async (i.e., you can return the Task directly), then don't use async.

There are some situations where return await is useful, like if you have two asynchronous operations to do:

var intermediate = await FirstAsync();
return await SecondAwait(intermediate);

For more on async performance, see Stephen Toub's MSDN article and video on the topic.

Update: I've written a blog post that goes into much more detail.

Upvotes: 152

heltonbiker
heltonbiker

Reputation: 27615

This also confuses me and I feel that the previous answers overlooked your actual question:

Why use return await construct when you can directly return Task from the inner DoAnotherThingAsync() invocation?

Well sometimes you actually want a Task<SomeType>, but most time you actually want an instance of SomeType, that is, the result from the task.

From your code:

async Task<SomeResult> DoSomethingAsync()
{
    using (var foo = new Foo())
    {
        return await foo.DoAnotherThingAsync();
    }
}

A person unfamiliar with the syntax (me, for example) might think that this method should return a Task<SomeResult>, but since it is marked with async, it means that its actual return type is SomeResult. If you just use return foo.DoAnotherThingAsync(), you'd be returning a Task, which wouldn't compile. The correct way is to return the result of the task, so the return await.

Upvotes: 4

Andrew Arnott
Andrew Arnott

Reputation: 81836

Making the otherwise simple "thunk" method async creates an async state machine in memory whereas the non-async one doesn't. While that can often point folks at using the non-async version because it's more efficient (which is true) it also means that in the event of a hang, you have no evidence that that method is involved in the "return/continuation stack" which sometimes makes it more difficult to understand the hang.

So yes, when perf isn't critical (and it usually isn't) I'll throw async on all these thunk methods so that I have the async state machine to help me diagnose hangs later, and also to help ensure that if those thunk methods ever evolve over time, they'll be sure to return faulted tasks instead of throw.

Upvotes: 12

Andrew Arnott
Andrew Arnott

Reputation: 81836

Another case you may need to await the result is this one:

async Task<IFoo> GetIFooAsync()
{
    return await GetFooAsync();
}

async Task<Foo> GetFooAsync()
{
    var foo = await CreateFooAsync();
    await foo.InitializeAsync();
    return foo;
}

In this case, GetIFooAsync() must await the result of GetFooAsync because the type of T is different between the two methods and Task<Foo> is not directly assignable to Task<IFoo>. But if you await the result, it just becomes Foo which is directly assignable to IFoo. Then the async method just repackages the result inside Task<IFoo> and away you go.

Upvotes: 25

svick
svick

Reputation: 245036

There is one sneaky case when return in normal method and return await in async method behave differently: when combined with using (or, more generally, any return await in a try block).

Consider these two versions of a method:

Task<SomeResult> DoSomethingAsync()
{
    using (var foo = new Foo())
    {
        return foo.DoAnotherThingAsync();
    }
}

async Task<SomeResult> DoSomethingAsync()
{
    using (var foo = new Foo())
    {
        return await foo.DoAnotherThingAsync();
    }
}

The first method will Dispose() the Foo object as soon as the DoAnotherThingAsync() method returns, which is likely long before it actually completes. This means the first version is probably buggy (because Foo is disposed too soon), while the second version will work fine.

Upvotes: 288

Servy
Servy

Reputation: 203812

The only reason you'd want to do it is if there is some other await in the earlier code, or if you're in some way manipulating the result before returning it. Another way in which that might be happening is through a try/catch that changes how exceptions are handled. If you aren't doing any of that then you're right, there's no reason to add the overhead of making the method async.

Upvotes: 31

Related Questions