4

See question title.

In other words, given a method

async Task FrobnicateAsync() { ... }

is there any (maybe subtle) difference between

async Task FrobAndFrobnicateAsync()
{
    Frob();
    await FrobnicateAsync();
}

and

Task FrobAndFrobnicateAsync()
{
    Frob();
    return FrobnicateAsync();
}
Heinzi
  • 167,459
  • 57
  • 363
  • 519

2 Answers2

2

If you literally have the code shown, then you should prefer the return FrobnicateAsync() version - it avoids an extra layer of state machine abstraction and is simply more efficient.

If you have additional things - in particular any finally (including using) code that surrounds the return, then you must be sure to use the await version so that the finally code doesn't happen until after the async task has completed.

If in doubt, use the async/await version.

The good news is: it doesn't impact the signature (so is not a breaking change) to switch between them, so you can implement it the more efficient way now, and change to async/await if you find that you need to add a using block later.

As an additional thought: if you usually expect it to be completed synchronously, you can be more subtle:

Task FrobAndFrobnicateAsync()
{
    async Task Awaited(Task t) => await t;
    Frob();
    var task = FrobnicateAsync();
    // in .NET vFuture there will be a task.IsCompletedSuccessfully
    return task.State == TaskState.RanToCompletion ? task : Awaited(task);    
}

This avoids the state machine when not needed.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • 1
    I'm not sure I'd always agree with that - if `Frob` might throw an exception, there's quite a significant different in making it async. See Stephen Cleary's blog post on this: https://blog.stephencleary.com/2016/12/eliding-async-await.html (In some cases I think it's fine, in others, not so much.) – Jon Skeet May 04 '17 at 10:05
  • @JonSkeet but you're not *absorbing* the difference; the *caller* should still be `await`-ing or whatever, so they'll get the exception *there* in the usual way – Marc Gravell May 04 '17 at 10:06
  • 1
    But that could be at a completely different point in their code. If this is a "bug in caller code" exception (e.g. validation) then I'm fine with that - if it's something like an `IOException`, then changing the point at which it's thrown feels more like a breaking change. – Jon Skeet May 04 '17 at 10:09
  • @JonSkeet ah, you mean exceptions inside `FrobAndFrobnicateAsync` before the call to `FrobnicateAsync`? Agreed: that is definitely a *difference*; it is a very complex conversation to discuss which version (if any) is *more correct*. Personally, I'm fine with it throwing in either way. – Marc Gravell May 04 '17 at 10:11
  • Yes, an exception in `Frob()`, exactly. – Jon Skeet May 04 '17 at 10:14
  • @JonSkeet I'm perhaps also biased because most of the time I'm dealing with `async` is in *library* code that wraps multiple (many) smaller async operations, and I want to avoid allocating a `Task`/`TaskCompletionSource`, or dealing with the state-machine overheads **whenever remotely possible**; in *application* code (with more "chunky" APIs): then I'd probably prefer the `await` version. – Marc Gravell May 04 '17 at 10:17
  • 1
    On the other hand, in application code you're more likely to know about your callers and be able to change their expectations around exceptions if necessary :) – Jon Skeet May 04 '17 at 10:19
  • @JonSkeet it's almost as though async was complex and nuanced... almost... – Marc Gravell May 04 '17 at 10:21
2

I have a blog post on eliding async.

Since you have a non-trivial Frob before the await, I recommend that you keep the async and await. The benefits of this approach are:

  • If there is no async and Frob throws an exception, then the exception is thrown directly, which is surprising behavior for the consumers. Even if Frob doesn't throw an exception today, it can do so next year; thus the code with await is more maintainable.
  • If there is no async, then Frob does not exist in the same logical call context as FrobnicateAsync (assuming it is actually async); instead, it would exist in the same logical call context as the caller of FrobAndFrobnicateAsync. This is not a concern in most code.

Also see What Is the Purpose of return await?

Community
  • 1
  • 1
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810