C#
Task Parallel Library
async programming
Task.Wait method
concurrency

Task.Factory.StartNew followed by Task.Wait

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

Task.Factory.StartNew() starts work asynchronously, but calling Task.Wait() immediately after it often removes most of the benefit by blocking the current thread until the task completes. In modern C#, this pattern usually signals that the code should use await, or that it may not need a task at all.

What the pattern actually does

Consider this code:

csharp
1using System;
2using System.Threading;
3using System.Threading.Tasks;
4
5Task task = Task.Factory.StartNew(() =>
6{
7    Thread.Sleep(1000);
8    Console.WriteLine("Background work finished");
9});
10
11task.Wait();
12Console.WriteLine("Caller resumed");

StartNew queues the delegate for asynchronous execution. Wait then blocks the calling thread until that task finishes. The result is concurrency at the task level, but not responsiveness at the caller level.

If the caller is a UI thread or an ASP.NET request thread, that blocking can be exactly what you were trying to avoid in the first place.

Why Wait() is often the wrong follow-up

Wait() is a synchronous blocking call. That means:

  • the current thread is stuck until the task completes
  • exceptions are wrapped in AggregateException
  • UI and request threads can become unresponsive

In older Task Parallel Library code, blocking was common. In modern async code, it is usually better to let the method remain asynchronous and use await.

csharp
1using System;
2using System.Threading.Tasks;
3
4static async Task RunAsync()
5{
6    await Task.Run(async () =>
7    {
8        await Task.Delay(1000);
9        Console.WriteLine("Background work finished");
10    });
11
12    Console.WriteLine("Caller resumed");
13}

Here, the caller does not block the thread while waiting for completion.

Prefer Task.Run for simple background work

Task.Factory.StartNew exposes a lot of configuration knobs, but that flexibility also makes it easier to misuse. For straightforward background work, Task.Run is usually the clearer API.

csharp
1using System;
2using System.Threading.Tasks;
3
4Task<int> task = Task.Run(() =>
5{
6    return 21 * 2;
7});
8
9int result = await task;
10Console.WriteLine(result);

Task.Run expresses the common case directly: queue work to the thread pool and await the result.

When blocking is acceptable

There are cases where Wait() is still reasonable:

  • console app entry points in older code
  • test harnesses
  • compatibility layers around synchronous APIs

Even then, you should understand the tradeoff. Blocking is not free, and it changes how exceptions and cancellation behave.

For example:

csharp
1try
2{
3    Task task = Task.Run(() => throw new InvalidOperationException("Boom"));
4    task.Wait();
5}
6catch (AggregateException ex)
7{
8    Console.WriteLine(ex.InnerException?.Message);
9}

With await, the original exception is usually easier to handle.

Why StartNew can surprise people

Task.Factory.StartNew predates async and await, and it has semantics that are easy to misuse. For example, if you pass it an async delegate, you can end up with nested tasks.

csharp
1Task<Task> outer = Task.Factory.StartNew(async () =>
2{
3    await Task.Delay(500);
4    Console.WriteLine("Done");
5});
6
7await outer.Unwrap();

That is one reason Task.Run is preferred for most modern async code. It handles common cases more predictably.

A better mental model

Choose the pattern based on what you want:

  • if work is naturally asynchronous, keep the whole path async and use await
  • if you need thread-pool offloading for CPU work, use Task.Run
  • if you block immediately with Wait, ask whether you needed a task at all

For example, if code is purely synchronous and you will wait right away, a direct method call may be simpler:

csharp
DoWorkSynchronously();

Creating a task only to block on it can add overhead and complexity without improving throughput or readability.

Common Pitfalls

The biggest mistake is calling Wait() on a UI or request thread. That can freeze the interface or reduce server scalability, and in some environments it can contribute to deadlock patterns.

Another issue is choosing Task.Factory.StartNew by habit when Task.Run is the simpler and safer option. StartNew is powerful, but it is not the default best practice for routine async work.

Developers also forget that Wait() changes exception handling. Instead of seeing the original exception directly, you often get AggregateException, which adds friction for no real gain.

Finally, a task that is created and then immediately waited on may indicate the code path does not need asynchronous structure at all. Sometimes the cleanest fix is to remove the task wrapper.

Summary

  • 'StartNew begins asynchronous work, while Wait() blocks the current thread for completion.'
  • Calling Wait() immediately often defeats the responsiveness benefits of task-based code.
  • Prefer await and Task.Run in modern C# unless you have a specific reason to block.
  • Blocking calls can hurt UI responsiveness, server scalability, and exception clarity.
  • If you always wait immediately, reconsider whether a task is necessary at all.

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.