In this article, we will learn how to run an async method synchronously in .NET.

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

Let’s start.

Different Ways We Can Run Asynchronous Methods Synchronously

First, let’s scaffold a simple console application in Visual Studio. Alternatively, we can create the project using the CLI command: 

dotnet new console

After that, let’s add a Person class with its properties:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }    
}

Having set up the project, let’s see how we can run an async method synchronously.

To recoup, an async method has the async keyword in its method signature and has the await keyword in the body. The await keyword holds the execution of the rest of the method until the asynchronous operation is complete. However, this happens without blocking the thread.

We use async methods for operations that don’t execute instantly, like fetching data from a remote server.

By running an async method synchronously, we block the current thread until the operation executes to completion. We have a great article on asynchronous programming with async and await in ASP.NET Core if this is a new topic for you.

We will be looking at different approaches to running an async method synchronously, namely:

  • Task.RunSynchronously()
  • Task.Wait()
  • Task.Result
  • GetAwaiter().GetResult()

Since we are discussing synchronous programming, please note that each of the approaches discussed in this article blocks the current thread until the results are available. We should do our best to avoid blocking asynchronous code because this could cause deadlocks as explained in Don’t Block on Async Code by Stephen Cleary.

We’ve got a lot to cover, so let’s dive in.

Using Task.RunSynchronously() to Run a Method Synchronously

We use the Task.RunSynchronously() method to execute a task in a synchronous manner. The tasks execute on the same thread one after another as determined by the TaskScheduler. Please refer to the article on the differences between tasks and threads for a detailed exploration.

In addition to that, calling the Task.RunSynchronously() method executes a task only once. In the event that task execution has already started, calling the Task.RunSynchronously() method before completion throws an InvaliOperationException

To demonstrate how it works, let’s create a new PersonService class and add a GetPeople() method:

public List<Person> GetPeople()
{
    var task = new Task<List<Person>>(() =>
    {
        Thread.Sleep(1000);

        return new List<Person>
        {
            new() { Name = "Alice", Age = 25 },
            new() { Name = "Bob", Age = 30 },
            new() { Name = "Charlie", Age = 35 }
        };
    });

    task.RunSynchronously();
    
    return task.Result;
}

First, we create a task that pauses the thread execution for 1 second before returning a list of Person objects. We have this 1-second delay to simulate a long-running operation, for instance, fetching data over a network. 

Calling the RunSynchronously() method, we wait for 1 second before getting the result back. This means if we had more operations that need to be run on the same thread they will be held for 1 second. For example, a UI thread will freeze for 1 second before being responsive again.

Now that we’ve covered the Task.RunSynchronously() method, let’s look at another alternative.

Using Task.Wait()

The Task.Wait() method similarly blocks the thread until a task is completed, is canceled, or has timed out. 

The method takes an optional timeout parameter which specifies the amount of time to wait for the task to complete before proceeding with program execution. In the event that task execution experiences a timeout, the Task.Wait() method returns false otherwise it returns true.

Also, if the task gets canceled during execution or it ends in a failed state, then it throws an exception wrapped in an AggregateException.

Let’s add an async method to the PersonService class:

private async Task<List<Person>> GetPeopleAsync()
{
    await Task.Delay(1000);

    return new List<Person>
    {
        new() { Name = "Alice", Age = 25 },
        new() { Name = "Bob", Age = 30 },
        new() { Name = "Charlie", Age = 35 }
    };
}

This method delays the task for 1 second before returning a list of Person objects. This way, we’re simulating an asynchronous operation.

Next, let’s add a new method to see Task.Wait() in action:

public List<Person> GetPeopleUsingWaitMethod()
{
    var task = GetPeopleAsync();
    task.Wait();

    return task.Result;
}

We first create a task by calling the GetPeopleAsync() method. Then, we call the Task.Wait() method, which blocks the thread until the task has been executed successfully. In this case, the thread is blocked for 1 second until the list of Person objects is returned. Finally, we read the results of the operation using Task.Result.

Using Task.Result to Synchronously Run a Method

The Task.Result property returns the result of a completed Task<T>

Let’s implement this by adding a new method to the PersonService class:

public List<Person> GetPeopleUsingResultMethod()
{
    var task = GetPeopleAsync();

    return task.Result;
}

We’re first creating a Task<List<Person>> task by calling the GetPeopleAsync() method. To get the results of the task, we call Task.Result which returns the list.

Task.Result also blocks when the result is not ready and doesn’t return immediately. But when the Result is ready it returns it immediately.

In case an exception is thrown, it is wrapped in an AggregateException.

Having seen how both Task.Wait() and Task.Result work, let’s look at an alternative approach.

Using GetAwaiter().GetResult()

The GetAwaiter().GetResult() method call is equivalent to calling Task.Wait() method and Task.Result. However, the difference is that GetAwaiter().GetResult() is preferred to the latter because it propagates exceptions instead of wrapping them in an AggregateException.

Let’s add a method that calls the GetPeopleAsync() method synchronously using the GetAwaiter().GetResult() method:

public List<Person> GetPeopleUsingGetAwaiter()
{
    var response = GetPeopleAsync().GetAwaiter().GetResult();

    return response;
}

Similarly, we are synchronously calling GetPeopleAsync() which is an asynchronous method. After we run our code, we have to wait for one second for the task to fully execute before getting the response. 

Exception Handling When Synchronously Calling an Async Method

The AggregateException wrapper that’s thrown when using Task.Wait() and Task.Result makes error handling difficult, that’s where GetAwaiter().GetResult() comes to the rescue. The GetResult() method checks for exceptions in the task, and if any, it throws them directly without having a wrapper around them.

This way, it’s easier for us to catch specific exceptions instead of catching a general AggregateException and checking the inner exceptions.

Having mentioned the differences in exception handling between Task.Wait(), Task.Result and GetAwaiter().GetResult(), let’s see how that in action.

To start off, let’s create a new async method that throws an exception:

public async Task ThrowExceptionAsync()
{
    await Task.Delay(1000);

    throw new InvalidOperationException("The task threw an invalid operation exception");
}

This method delays task execution for 1 second before throwing an InvalidOperationException.

Let’s add a new method to demonstrate AggregateException:

public void ExceptionHandlingUsingWaitMethod()
{
    var task = ThrowExceptionAsync();
    try
    {
        task.Wait();
    }
    catch (AggregateException e)
    {
        foreach (var innerException in e.InnerExceptions)
        {
           Console.WriteLine(innerException.Message);
           throw;
        }
    }
}

First, we create a task and assign it from the ThrowExceptionAsync() method. Then, in the try-catch block, we call the Wait() method which will block the thread for 1 second and then it will throw an Exception an InvalidOperationException.

Since we’re using the Wait() method, the InvalidOperationException is wrapped in an  AggregateException. To get the actual error message, we loop over the InnerExceptions in the AggregateException. We would do the same if we were using Task.Result.

If we have multiple cases to handle for each type of exception, we will have to write additional code and this easily complicates it.

Let’s see how we can handle exceptions when using GetAwaiter().GetResult() method:

public void ExceptionHandlingUsingGetAwaiterMethod()
{
    var task = ThrowExceptionAsync();
    try
    {
       task.GetAwaiter().GetResult();
    }
    catch (InvalidOperationException e)
    {
        Console.WriteLine($"Error Message: {e.Message}");
        throw;
    }
}

Similar to the first approach, we also use the try-catch block. The only difference is that we handle the exceptions directly using the Exception namespace. The latter is a simpler approach compared to when we used the Task.Wait() method.

Conclusion

In this article, we have learned how to run an async method synchronously in .NET. We have looked at the different ways to do this, and how each approach is similar or different from the other.

We have also covered exception handling, more specifically, using the AggregateException and Exception types. However, all these approaches block the thread execution and therefore could result in deadlocks. If necessary, use them with a lot of caution.