Exceptional Tasks

I am just putting this in a blog for now so I have it somewhere. There probably is a way better blog out there, but I need to also write a blog post so I never forget again.

So… What are we talking about today? Pretty simple thing really: how to move a method to a background thread, and still catch exceptions.

This example is very simple, so assume we have this method:

string GetName()
{
    try
    {
        return ExpensiveNameGetter();
    }
    catch
    {
        return null;
    }
}

Let’s ignore the fact that this should not be the case and pretend we have no other method we can call. The issue is, the expensive method is… well… expensive. So, our goal is to move this off the main/UI thread and onto some background thread pool. We have heard of Task, so let’s use that!

async Task<string> GetName()
{
    try
    {
        return await Task.Run(() => { return ExpensiveNameGetter(); });
    }
    catch
    {
        return null;
    }
}

Yay! Our code now runs in the background thread and all we had to do was make it a Task and then we are done! Well, this is where another thing comes in… the async and await keywords. These keywords are all syntactic sugar to generate a fair bit of code. This is great because we don’t have to worry about how it all works, but it is also unnecessary.

You can see all the code it generates using this sharplap.io link.

One way we can solve this is to remove the async/await and return tasks and results (using Task.FromResult):

Task<string> GetName()
{
    try
    {
        return Task.Run(() => { return ExpensiveNameGetter(); });
    }
    catch
    {
        return Task.FromResult<string>(null);
    }
}

But wait! This is all lies! Let’s look at the code again. What is happening in the try block? We are running a task you might say, and that is correct… -ish… Are you really running the task, or are you starting the task? If you look at the block before, we awaited the result of the task, but now we are not.

So what is really happening here? Exactly what you told it to do: “start this task, and if there are any exceptions, return null”. But, is this what we want? No. This is because there is a subtle difference in the desire of “run and await this task, and if there are any exceptions, return null”.

The original block was awaiting the task to complete and would handle any exceptions during the run. This new block is just starting the task and handling any exceptions that occurred during the starting of that task!

This may sound like a bug or a missing feature, but in fact it is desired. What we are saying is that we want to execute some operation and return immediately, leaving the operation to continue – on a background thread/task. If we were to catch the exceptions that occur in the task, then we would have to block this method and wait for it to complete to be sure… and then we end up right where we started.

So… how do we handle this? How can we use background tasks and also catch exceptions – but not require a state machine? Very simple: put the try/catch inside the Task.Run:

Task<string> GetName()
{
    return Task.Run(() =>
    {
        try
        {
            return ExpensiveNameGetter();
        }
        catch
        {
            return null;
        }
     });
}

That’s it! Just a quick note to myself and maybe a reminder to anyone reading this to either make sure you are awaiting your tasks or catching the right exceptions.

In most cases, you should await, but in some cases all you do are running some single operation on a background thread. No need to spin up a state machine just for that!