0

When passing through method calls to another async method, should the caller method also be async and use await, or should I simply pass through the Task it receives from the callee? What if the calling method performs a bit more preparation?

public Task<Message> Unsubscribe(int subscriptionId, CancellationToken cancellationToken)
{
    var data = new MessageData
    {
        ["subscriptionId"] = subscriptionId
    };
    return SendAsync(OpCode.Unsubscribe, data, cancellationToken);
}

public Task<Message> Unsubscribe(int subscriptionId) =>
    Unsubscribe(subscriptionId, CancellationToken.None);

SendAsync is async and returns Task<Message>. So should the first overload of Unsubscribe be like above or like that:

public async Task<Message> Unsubscribe(int subscriptionId, CancellationToken cancellationToken)
{
    var data = new MessageData
    {
        ["subscriptionId"] = subscriptionId
    };
    return await SendAsync(OpCode.Unsubscribe, data, cancellationToken);
}

The other alternative is with the second overload of Unsubscribe. It might be like above or like that:

public async Task<Message> Unsubscribe(int subscriptionId) =>
    await Unsubscribe(subscriptionId, CancellationToken.None);

I guess that more asyncs and awaits add complexity introduced by the compiler (I see it in the stack traces!) and may degrade performance and memory consumption. But at least it should provide for a consistent exception propagation.

ygoe
  • 18,655
  • 23
  • 113
  • 210
  • There are other subtle differences. In particular, you should use `ConfigureAwait(false)`; otherwise, your second version might be prone to deadlocks. – Douglas Jun 23 '18 at 14:14
  • [Async/Await - Best Practices in Asynchronous Programming](https://msdn.microsoft.com/en-us/magazine/jj991977.aspx) –  Jun 23 '18 at 14:25
  • _"SendAsync is async"_ is an internal affair of SendAsync(). For the caller it is an awaitable. So are both versions of Unsubscribe(). – H H Jun 23 '18 at 16:46
  • There are more similar questions but the dupe I picked offers the better explanation and links. – H H Jun 23 '18 at 16:46

1 Answers1

2

In the examples you cited, just returning the task without awaiting it is fine (and arguably preferable), but this does require some care.

Once case were you can get into trouble is when you're dealing with Tasks inside a using block. These can have vastly different behaviors:

public async Task<Something> AwaitTheTask()
{
    using (var someResource = GetAResource())
    {
        return await SomeAsyncThing(someResource);
    }
}

public Task<Something> DontAwaitTheTask()
{
    using (var someResource = GetAResource())
    {
        return SomeAsyncThing(someResource);
    }
}

In the first example, the using block will not dispose someResource until the awaited Task has completed. In the second example, someResource will be disposed right away, very likely causing problems for the code that needs that resource.

JLRishe
  • 99,490
  • 19
  • 131
  • 169
  • Thank you. To me, it's obvious that your two samples behave differently because something's happening after the `await`, which will happen earlier if I don't wait. – ygoe Jun 23 '18 at 14:31
  • @ygoe Well, it wasn't immediately obvious to me and I had to learn this the hard way (^^); It's easy to forget things like that if one doesn't know to look for them. – JLRishe Jun 23 '18 at 15:24