C#
Task.Factory.StartNew
multithreading
method parameters
asynchronous programming

Passing a method parameter using Task.Factory.StartNew

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

You can pass a method parameter to Task.Factory.StartNew either by capturing it in a lambda or by using the overload that accepts an object state value. Both work, but they are not equally readable or equally appropriate in modern .NET code.

The bigger point is that StartNew is not the default recommendation for simple asynchronous work anymore. In many cases, Task.Run is clearer unless you specifically need StartNew options or scheduler control.

The Most Readable Pattern: Use a Lambda

For straightforward parameter passing, a lambda is usually the cleanest approach.

csharp
1using System;
2using System.Threading.Tasks;
3
4class Program
5{
6    static void PrintMessage(string message)
7    {
8        Console.WriteLine(message);
9    }
10
11    static async Task Main()
12    {
13        string value = "Hello from a task";
14
15        Task task = Task.Factory.StartNew(() => PrintMessage(value));
16        await task;
17    }
18}

The lambda closes over value, so the method receives the parameter naturally.

Use the State Overload When You Need It

StartNew also has an overload that accepts a state object.

csharp
1using System;
2using System.Threading.Tasks;
3
4class Program
5{
6    static void PrintMessage(object? state)
7    {
8        string message = (string)state!;
9        Console.WriteLine(message);
10    }
11
12    static async Task Main()
13    {
14        Task task = Task.Factory.StartNew(PrintMessage, "Hello from state");
15        await task;
16    }
17}

This avoids creating a closure, but the downside is loss of type safety because the parameter arrives as object and usually requires a cast.

For most application code, the lambda version is easier to read and maintain.

Multiple Parameters

If the method takes several values, the lambda approach stays simple.

csharp
1using System;
2using System.Threading.Tasks;
3
4class Program
5{
6    static void PrintOrder(int id, string status)
7    {
8        Console.WriteLine($"Order {id}: {status}");
9    }
10
11    static async Task Main()
12    {
13        int id = 42;
14        string status = "Processed";
15
16        Task task = Task.Factory.StartNew(() => PrintOrder(id, status));
17        await task;
18    }
19}

Trying to force multiple values through the single object state parameter usually makes the code less clear, not more.

Why Task.Run Is Often Better

If your only goal is to offload work to the thread pool, Task.Run is usually the more modern choice.

csharp
Task task = Task.Run(() => PrintMessage("Hello from Task.Run"));
await task;

Task.Factory.StartNew exposes more options, but it also has more footguns around schedulers and async delegates. That is why many developers now use Task.Run for simple background work and reserve StartNew for advanced cases.

Be Careful with Async Delegates

One common trap is using StartNew with an async lambda and expecting it to behave like Task.Run.

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

Because StartNew with an async lambda produces a nested Task<Task>, you often need Unwrap(). This surprises many people. Task.Run handles this more naturally.

When StartNew Still Makes Sense

Task.Factory.StartNew still matters when you need:

  • a custom task scheduler
  • long-running task hints
  • attached child-task behavior
  • fine-grained creation options

If you do not need those features, prefer the simpler API.

Common Pitfalls

  • Using the object state overload everywhere and filling the code with fragile casts.
  • Reaching for StartNew when Task.Run would be simpler and clearer.
  • Forgetting that StartNew with an async delegate returns a nested task.
  • Assuming parameter passing is the hard part when the real issue is task-scheduler behavior.
  • Capturing mutable variables in closures without understanding when their values may change.

Summary

  • The easiest way to pass parameters to Task.Factory.StartNew is usually a lambda.
  • 'StartNew also supports a state-object overload, but it is less type-safe.'
  • For multiple parameters, lambdas stay cleaner than packing values into object.
  • 'Task.Run is often the better default for simple thread-pool work.'
  • Use StartNew when you genuinely need its advanced scheduling or task-creation options.

Course illustration
Course illustration

All Rights Reserved.