In this article, we will discuss the differences between await and Task.Wait() in C# and .NET. We will start by looking into concepts like Blocking Code and Asynchronous Programming. We will see how we can use await and Task.Wait() in our code and their effects on our applications. Finally, we will suggest which one we should be using and why. 

To download the source code for this article, you can visit our GitHub repository.

So, let’s start.

Why Blocking Code Is Bad

We write code to build applications. Our applications are sets of commands hosted in OS processes. More precisely, each process utilizes a limited number of threads, managed by a thread pool, to execute the various commands of our applications.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

Every command in our software executes in a thread:

The operating system uses processes to run our applications. Each process spawns multiple threads from a thread pool

As mentioned, there is a limit on the maximum number of threads that a process can have. Therefore, we should ensure that we utilize threads efficiently. That is, our code should only reserve a thread for as little as possible and free up threads, which means returning them to the thread pool, as soon as they are no longer required.

However, there are commands that by nature will require much time to complete.

There are 2 categories of such long operations:

  • CPU-intensive: Heavy operations that stress out the CPU and require significant time to complete. Examples of such operations are complex mathematical calculations and video or image compression.
  • I/O-intensive: Operations that heavily rely on external and remote resources and involve significant wait times. Examples include writing to/reading from the database and calling Web APIs.

Blocking code is code that reserves a thread for the whole period until a CPU or I/O intensive operation completes. This is often a long period and such code often leads to inefficiencies, unresponsiveness, and bad user experience.

As developers, we should avoid writing blocking code. And we have the tools to achieve that. 

Understanding Asynchronous Programming for await and Task.Wait

Asynchronous Programming is a programming paradigm that we can use to avoid blocking threads in our application. The execution of long-running CPU and I/O intensive operations moves to a separate flow from the main execution flow of our application. The threads executing those long-running operations do not block but, they are freed up and used in other tasks until the long-running operation is completed. At that point, code execution resumes with the next operation. This way our system remains responsive and the overall perception is that our application runs faster. 

In the latest .NET versions, asynchronous programming is achieved via Task-based Asynchronous Pattern, and the keywords await and async.

You can read more on those in our Asynchronous Programming with Async and Await in ASP.NET Core article together with a more extensive analysis of Asynchronous Programming.

Now that we have learned the fundamentals of blocking and asynchronous code, let’s continue by seeing the effects that Task.Wait() and await have in our applications and how these relate to the above concepts.

Asynchronous Programming and Task.Wait

Task is a class in the System.Threading.Tasks namespace that represents an asynchronous operation. When we want to run some operation asynchronously then we can wrap it in a Task object and start it. Task exposes lots of useful functionality through its interface that allows us to interact with it and the running operation. One of the methods that we can use on Task objects is Task.Wait().

When we call Task.Wait() we instruct the runtime to stop and wait for the Task to complete its execution, causing the execution to block. That means on stopping, the running thread is not freed up. Rather it stays occupied, waiting for the operation to complete. This also means that the thread running our code before calling Wait() will also run our code after the call. We understand that calling Task.Wait() has all the negatives that we mentioned earlier in the article about blocking code. 

Let’s see how we can call the Task.Wait() method on a running Task:

public static void BlockingCodeExample()
{
    Console.WriteLine("** Blocking code Example **");

    Console.WriteLine($"Current ManagedThreadId: {Environment.CurrentManagedThreadId}");

    Task taskToBlock = Task.Run(() =>
    {
        Thread.Sleep(5000);

        return "Hello, from a blocking task";
    });

    taskToBlock.Wait();

    string blockingResult = taskToBlock.Result;
    Console.WriteLine($"Current ManagedThreadId: {Environment.CurrentManagedThreadId}");
}

Here, we create the BlockingCodeExample() method that initially prints, in the console, the CurrentManagedThreadId of the executing thread. Then, we define an object of type Task, which wraps a Func delegate method. The delegate method calls Thread.Sleep() to mock a long-running operation that takes 5 seconds to complete and returns a string.

Then, we call the Wait() method on the Task that causes the executing thread to block while waiting for the completion of the long-running operation. After that, we call the Result property of  taskToBlock to retrieve the result of the task.

Finally, we print again the CurrentManagedThreadId, noticing that the thread is the same.

Let’s check the console output:

** Blocking code Example **
Current ManagedThreadId: 1
Current ManagedThreadId: 1

Let’s continue with the await operator to see how it differs from what we’ve seen so far.

Asynchronous Programming and await

The await operator is used to wait for an operation to complete asynchronously. As soon as the runtime notices that a Task is awaited, execution stops and the running thread is freed up until the asynchronous operation completes. Until that happens, the runtime can use the thread in other operations and the system remains responsive.

Additionally, the await keyword also “unwraps” the returned value of the asynchronous operation upon its completion and lets us assign it to a variable.

The await operator can only be used on asynchronous methods marked with the async operator. It is also a best practice the method to have a return type of Task or Task<T>. This way the caller of the method can also use await. Event handlers are good to be marked async with a void return type.

Let’s now see how we can use the await keyword in an asynchronous method:

public async static Task AsynchronousCodeExampleAsync()
{
    Console.WriteLine("** Asynchronous code Example **");

    Console.WriteLine($"Current ManagedThreadId: {Environment.CurrentManagedThreadId}");

    Task taskToAwait = Task.Run(() =>
    {
        Thread.Sleep(5000);

        return "Hello, from an asyncrhonous task";
    });

    string awaitResult = await taskToAwait;
    
    Console.WriteLine($"Current ManagedThreadId: {Environment.CurrentManagedThreadId}");
}

Here, we declare the AsynchronousCodeExample() method marking it async so that we can use the await operator inside it. We print the CurrentManagedThreadId of the executing thread and we define the Task that we will await, mocking a long-running operation which we do by calling Thread.Sleep().

Then we use the await keyword to asynchronously wait for the completion of the Task and to receive its result. Notice that in this case, we don’t need to use the Result property as we did before. Finally, we print again the CurrentManagedThreadId.

Let’s see the output of our method:

** Asynchronous code Example **
Current ManagedThreadId: 1
Current ManagedThreadId: 10

Notice that in this case, CurrentManagedThreadId has changed value after calling the await keyword. This is because the runtime decides to switch the executing thread. As mentioned earlier, the initial thread is freed up instead of blocking while waiting for completion. It is important to note that depending on the application type and our code, the runtime might wait for the initial thread to be free again to continue execution after the completion of the long-running operation.

Exception Handling With await and Task.Wait

Now that we’ve discussed the differences between await and Task.Wait() in the execution flow, let’s also see how they differ in the way that they handle exceptions.

As we’ve seen the await keyword returns the result of an asynchronous operation on its completion. In addition to that, it also checks if any Exception has been raised within the asynchronous operation and throws the Exception back to the main thread. Therefore, the experience that we get is very similar to the one of synchronous execution.

Let’s now see this in action:

public async static Task AsynchronousExceptionHandlingAsync()
{
   Console.WriteLine("** Asynchronous Exception Handling **");

    try
    {
        Task taskToFail = Task.Run(() =>
        {
            throw new ApplicationException("Error in the asynchronous long-running operation");
        });

        await taskToFail;
    }
    catch (ApplicationException ex)
    {
        Console.WriteLine(ex.Message);
    }
}

Here, we define the AsynchronousExceptionHandling() method which runs a Task that raises an ApplicationException. We use a try-catch block to wrap the code and handle any exceptions.

Now, let’s run this code and we should see that we successfully catch the ApplicationException:

** Asynchronous Exception Handling **
Error in the asynchronous long-running operation

Let’s now see how the same code behaves when we use Task.Wait():

public static void BlockingExceptionHandling()
{
    Console.WriteLine("** Blocking Exception Handling **");

    try
    {
        Task taskToFail = Task.Run(() =>
        {
            throw new ApplicationException("Error in the blocking long-running operation");
        });

        taskToFail.Wait();
    }
    catch (ApplicationException ex)
    {
        Console.WriteLine(ex.Message);
    }
}

Here, we define the BlockingExceptionHandling() method which runs a Task that raises an ApplicationException. The only difference this time is that we don’t await to the result, instead calling the Wait() method.

We run this code but we notice that it crashes!

Let’s see the output:

Shows the exception thrown by the blocking exception handling.

It looks like our code throws a System.AggregateException instead of the ApplicationException that we expected. Task-based programming uses the AggregateException to consolidate multiple exceptions that parallel tasks might throw. It is a powerful concept when we deal with concurrent Tasks and we want to check their status and report their failures. However, when handling errors in a single asynchronous task, AggregateException adds complexity to our code without any benefits.

Conclusion

In this article, we’ve analyzed the differences between the await operator with the Task.Wait() method. We started by discussing what is blocking code, why is it a bad practice, and how we can use asynchronous code instead. We’ve seen how we can use await and Task.Wait() in our code and how they affect the execution flow of our programs. We’ve seen how await makes our Exception handling easier and why we should use it to build efficient and responsive systems.  

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!