Programming  /  C# September 09, 2019

Exception order when awaiting multiple async tasks in C#

A C# has-been returns to C# and experiments with this new hip thing called async/await and how that relates to execution order and exceptions.

I recently returned to .NET and C# development after a six year hiatus in Node.JS land. A lot has changed since I last wrote C#, although Jon Skeet still writes the best books.

One feature that I never got to use during my previous C# development days were async/await. However, I'm very familiar with the concept having written a lot of JavaScript with promises and async/await. However, one thing that I didn't intuitively knew in C# was in what order Exceptions are thrown, or rather caught, when awaiting multiple Tasks. Therefor I created this little experiment: 

using System;
using System.Threading;
using System.Threading.Tasks;

namespace AsyncExceptions
{
  class Program
  {
    async static Task Main(string[] args)
    {
      try
      {
        var one = Do("One", 500);
        var two = Do("Two", 500);

        await one;
        await two;
       } catch(Exception ex)
       {
          Console.WriteLine(ex.Message);
        }
           
      }
      
      public static Task Do(string name, int time)
      {
        return Task.Run(() =>
        {
          Console.WriteLine($"Task {name} starting");
          Thread.Sleep(time);
          Console.WriteLine($"Task {name} pre exception");
          throw new Exception($"Exception {name}");
        });
      }
  }
}

Note: This is a contrived example that intrigues me and which we are exploring in this article. In practice you shouldn’t write code like the one above, with two separate awaits but instead use Task.WhenAll().

The above program will output five lines in total. Two lines will be printed for when each task starts to run. Two additional lines will be outputted when each task has waited for 500 milliseconds and is about to throw. Finally there will be a line telling us which exception was caught. Can you guess what the output will be?

As it turned out in my experiments the first four lines are quite random. Sometimes the first tasks starts and completes first, sometimes it's the other way around. Here are three sample outputs (the final line omitted for now):

Sample 1:

Task One starting
Task Two starting
Task Two pre exception
Task One pre exception

Sample 2:

Task One starting
Task Two starting
Task One pre exception

Sample 3:

Task Two starting
Task One starting
Task Two pre exception
Task One pre exception

What about the final line? That's always reads `Exception One`. Meaning that although the second exception may sometimes be thrown first in the async context back in our main thread we'll always catch the exception from the task that we await first. That holds true even if we make the second task throw much sooner by modifying the Main method to look like this:

var one = Do("One", 500);
var two = Do("Two", 1);

Of course in hindsight of this experiment the result makes sense. While the second async operation may complete and be ready to return to the initiating thread first in that thread we are waiting for the first task to complete before caring about the result of the second one.  

JavaScript

So what about the equivalent code i JavaScript? It could look something like this:

"use strict";

async function main() {
    try {
        const one = Do("One", 500);
        const two = Do("Two", 500);

        await one;
        await two; 
    } catch(ex) {
        console.log(ex.message)
    }
}

main();

function Do(name, time) {
    return new Promise((resolve, reject) => {
        console.log(`Task ${name} starting`)
        setTimeout(() => {
            console.log(`Task ${name} pre exception`);
            reject(new Error(`Exception ${name}`))
        }, time);
    })
    
}

Note: Again this is a contrived example. In practice you should use Promise.all() in code like this.

Here the output is more consistent. In all my attempts the output read like this:

Task One starting
Task Two starting
Task One pre exception
Task Two pre exception
Exception One
(node:6564) UnhandledPromiseRejectionWarning: Error: Exception Two

The first task is put queue for execution on the event loop first and therefor is executed and throws first. We catch the first exception and then get an angry error message due to us not catching the second exception. Clearly Promise.all would be a good solution for that.

But what if we change the second task to complete much faster than the first one?

const one = Do("One", 500);
const two = Do("Two", 1);

Then the output is this:

Task One starting
Task Two starting
Task Two pre exception
(node:6577) UnhandledPromiseRejectionWarning: Error: Exception Two
Task One pre exception
Exception One

The second task throws much earlier and that results in a an UnhandledPromiseRejectionWarning. Then the execution continues and the exception thrown by the first, and first awaited, task is caught. While I find this fascinating I think I'll leave the "why!?" to a different blog post.

PS. For updates about new posts, sites I find useful and the occasional rant you can follow me on Twitter. You are also most welcome to subscribe to the RSS-feed.

Joel Abrahamsson

Joel Abrahamsson

I'm a passionate web developer and systems architect living in Stockholm, Sweden. I work as CTO for a large media site and enjoy developing with all technologies, especially .NET, Node.js, and ElasticSearch. Read more

Comments

comments powered by Disqus

More about C#