Search code examples
c#asynchronousasync-awaittask-parallel-library

Understanding multiple consecutive await statements


I was playing around with the ContinueWith function and I ended up not understanding it.

In this example code:

var s = Task.FromResult(true).ContinueWith(async t => t).ContinueWith(async t => t);
await await await await await s;

What does each of the "awaits" await?


Solution

  • Let's answer in a very naive manner first:

    var s = Task.FromResult(true).ContinueWith(async t => t ).ContinueWith(async t => t);
    await await await await await s;
    

    First, let's just split the expression into its constituent parts:

    var s1 = Task.FromResult(true);
    var s2 = s1.ContinueWith(async t => t );
    var s3 = s2.ContinueWith(async t => t);
    

    if we expand the two first var declarations with full type, we get this:

    Task<bool> s1 = Task.FromResult(true);
    Task<Task<Task<bool>>> s2 = s1.ContinueWith(async t => t );
    

    The first is simple, it is a task that will yield a boolean value. The next need some explanation.

    When you do task.ContinueWith(...), you have an overload that takes in the task that was preceeding the ... part, in this case, the task reference. This task gets passed into the continuation method, so this:

    s1.ContinueWith(async t => t)
    

    means that s1 will be passed into the async t => t continuation as t, and then it will be returned.

    Additionally, when you do:

    var x = task.ContinueWith(...)
    

    you get back a task that can be awaited, to get the return value from .... In this case it is another task because you just returned the task that was passed in.

    So this means that this expression:

    var s2 = s1.ContinueWith(async t => t );
    

    will actually return a task, from ContinueWith, and this task wraps another task, from t => t. So you can await the task from ContinueWith to get the s1 task, which can also be awaited.

    Let's expand the expressions fully:

    Task<bool> s1 = Task.FromResult(true);
    Task<Task<Task<bool>>> s2 = s1.ContinueWith(async t => t );
    Task<Task<Task<Task<Task<bool>>>>> s = s2.ContinueWith(async t => t);
    

    each level adds another "task that wraps another task that wraps ...".

    When we unwrap the 5-level await, we get this:

    • first we await the final ContinueWith
    • this produces a task, from async t = t, so we await this
    • this produces a task, the first ContinueWith, so we await this
    • this produces a task, the first async t = t, so we await this
    • this produces a task, the original Task.FromResult(true)

    so the final code can be looked at like this:

    var s = Task.FromResult(true).ContinueWith(async t => t ).ContinueWith(async t => t);
                        ^                ^         ^                ^             ^
                        |                |         |                |             |
                    await       await  await       |    await    await s;         |
                                    |              |      |                       |
                                    +--------------+      +-----------------------+
    

    Note that all this happens synchronously, there is no waiting, no async code at all here. This is because all the tasks involved complete inline immediately, producing their results.

    In general, ContinueWith is something best left for framework and library authors, and not really a construct we're meant to be using any more. There are many things you need to be aware of, in particular around exception handling, that async/await just does naturally for you without hassle.