Overview
System.Threading.Tasks.Task is the central building block of the Task Parallel Library (TPL) in .NET. A Task represents a unit of work that runs asynchronously, usually on a thread pool thread. Task<TResult> is the same idea for work that produces a value.
Instead of managing raw Thread objects, a Task lets the runtime decide which thread pool thread executes the work, tracks the work's progress through a Status property, and gives you many ways to wait for it, combine it with other tasks, read its result and observe its errors.
This article walks through the everyday members of the Task class with two examples:
- Core concept example (
Run()) – six short parts that together touch the members you will use almost every day: creating tasks, reading status and properties, waiting, combining tasks, reading exceptions and cancelling a task. - Real-world example (
RunPaymentTimeoutScenario()) – a small checkout flow that adds a timeout to a slow operation with Task.WhenAny and Task.Delay, one of the most common real-world uses of the Task class.
Features and Design
Creating a task
Task.Run(Action) / Task.Run(Func<TResult>) – the standard way to queue work on a thread pool thread. It always behaves as if TaskCreationOptions.DenyChildAttach were set, so any child task started inside it is automatically detached.Task.Factory.StartNew(...) – the older, more configurable way to start a task. It accepts TaskCreationOptions such as LongRunning or AttachedToParent, which Task.Run does not expose. Prefer Task.Run unless one of those extra options is genuinely needed.Task.FromResult(value) – wraps an already known value in a task that is completed immediately. Useful when a method must return Task<TResult> but the value is already available.Task.FromException(exception) / Task.FromCanceled(token) – the same idea as FromResult, but for a task that is already faulted or already cancelled.Task.CompletedTask – a ready-made, already finished Task with no value, handy for a synchronous branch inside a method that must still return a Task.Task.Delay(TimeSpan) – a task that completes after the given time, without blocking any thread while it waits.
Reading status and properties
Id – a number that uniquely identifies the task instance for its lifetime; useful in logs when several tasks run at once.Status – a TaskStatus value such as Created, WaitingToRun, Running, RanToCompletion, Canceled or Faulted.IsCompleted – true once the task has left the running state, no matter whether it succeeded, failed or was cancelled.IsCompletedSuccessfully – true only when Status is RanToCompletion. This is the safest single flag to check before reading Result.IsFaulted / IsCanceled – true when the task ended with an unhandled exception, or when it observed a cancellation request.Result (on Task<TResult> only) – the value produced by the task. Reading it blocks the calling thread until the task finishes, and rethrows the task's error wrapped in an AggregateException if the task faulted.
Waiting for tasks
task.Wait() – blocks the calling thread until that one task finishes.task.Wait(TimeSpan) / task.Wait(int milliseconds) – blocks only up to the given time and returns a bool that tells whether the task actually finished in time.Task.WaitAll(params Task[]) – blocks until every listed task has completed.Task.WaitAny(params Task[]) – blocks until the first listed task completes and returns its index in the array.
All of these are blocking calls: they occupy the calling thread while they wait. In asynchronous code, prefer await, Task.WhenAll or Task.WhenAny instead, which do not block a thread while waiting.
Combining tasks
Task.WhenAll(...) – returns one task that completes when all given tasks have completed. For Task<TResult> inputs, its Result is an array with every individual result, in the same order as the inputs.Task.WhenAny(...) – returns one task that completes as soon as the first given task completes. Its Result is the winning task itself, not the value directly, so the value still has to be read through .Result on that winner.
WhenAll/WhenAny do not block a thread while waiting, which makes them the preferred choice inside async methods, usually together with await.
Reading errors
The Exception property lets code inspect a faulted task without triggering another throw. It is always an AggregateException, even when only a single exception was thrown inside the task. Calling Flatten() removes nested AggregateException layers, and InnerException reaches the real error.
Cancelling a task
Most task-creating methods accept an optional CancellationToken. Passing the token alone does not stop a running loop by itself – the task body must check the token, for example with token.ThrowIfCancellationRequested(), which throws OperationCanceledException as soon as cancellation has been requested. A task that reacts to its token this way ends in the Canceled status.
Other members worth knowing
ContinueWith(...), Task.Factory.ContinueWhenAll(...) and Task.Factory.ContinueWhenAny(...) – attach follow-up work to one or several tasks; covered in depth in a dedicated article about task continuations.TaskCreationOptions.AttachedToParent and child tasks created inside Task.Factory.StartNew – covered in depth in a dedicated article about child tasks.Task.Yield() – inside an async method, forces the rest of the method to continue asynchronously instead of running synchronously; mostly useful for fairness in long loops.task.GetAwaiter() / ConfigureAwait(bool) – the machinery behind the await keyword; ConfigureAwait(false) tells the continuation it does not need to resume on the original context.task.RunSynchronously() – runs a task that was created (not started) with new Task(...) on the calling thread; rarely needed in everyday code.task.Dispose() – releases the wait handle a task may allocate internally; in practice most tasks are short-lived and do not need to be disposed explicitly.
Example 1 – Core Concept: The Everyday Members of Task
The first example is split into six short parts, each one focused on a small group of related members, so every idea is easy to connect back to a single line of console output.
Code Example
/// <summary>
/// Core concept example.
/// Walks through the everyday members of the Task class: two ways to start a task,
/// two ways to get an already-finished task, reading its properties, three ways to
/// wait for it, two ways to combine several tasks, reading a faulted task's error,
/// and stopping a task with a CancellationToken.
/// </summary>
public static void Run()
{
Console.WriteLine("=== The Task class: core concept ===");
Console.WriteLine();
// ----------------------------------------------------------------------------
// Part 1: creating tasks.
// ----------------------------------------------------------------------------
Console.WriteLine("--- Part 1: creating tasks ---");
// Task.Run queues the given work on a thread pool thread and returns immediately.
// It is the most common way to start CPU-bound or blocking work in the background.
// 100 ms only makes the work visible; it is short enough to keep the demo fast.
Task<int> squareResult = Task.Run(() =>
{
Thread.Sleep(100);
const int number = 6;
Console.WriteLine($"[Task.Run] Calculating the square of {number}.");
return number * number;
});
// Task.Factory.StartNew is the older, more configurable way to start a task.
// It accepts TaskCreationOptions (for example LongRunning or AttachedToParent),
// which Task.Run does not expose directly. Prefer Task.Run unless one of those
// extra options is actually needed.
Task<int> cubeResult = Task.Factory.StartNew(() =>
{
Thread.Sleep(100);
const int number = 3;
Console.WriteLine($"[Task.Factory.StartNew] Calculating the cube of {number}.");
return number * number * number;
});
// Task.FromResult wraps an already known value in a completed task. It is useful
// when a method must return a Task<T> but the value is available immediately,
// without doing any real asynchronous work.
Task<int> cachedResult = Task.FromResult(42);
// Task.CompletedTask is a ready-made, already finished Task with no value. It is
// handy for a synchronous code path inside a method that must still return a Task.
Task noWorkNeeded = Task.CompletedTask;
// Task.Delay creates a task that completes after the given time span, without
// blocking any thread while it waits. 150 ms is used only to make the delay visible.
Task pause = Task.Delay(150);
Task.WaitAll(squareResult, cubeResult, noWorkNeeded, pause);
Console.WriteLine($"[Task.FromResult] Cached result is {cachedResult.Result}.");
Console.WriteLine();
// ----------------------------------------------------------------------------
// Part 2: task status and properties.
// ----------------------------------------------------------------------------
Console.WriteLine("--- Part 2: task status and properties ---");
// A simple division stands for "some calculation"; 200 ms makes the task still be
// running when the first status line below is printed.
Task<int> slowCalculation = Task.Run(() =>
{
Thread.Sleep(200);
return 10 / 2;
});
// Right after creation the task is almost always still running, so IsCompleted is
// false and Status is WaitingToRun or Running.
Console.WriteLine(
$"[before wait] Id={slowCalculation.Id}, Status={slowCalculation.Status}, IsCompleted={slowCalculation.IsCompleted}");
slowCalculation.Wait();
// After Wait() returns, the task has finished successfully, so these flags switch
// to their final values. IsCompletedSuccessfully is true only for RanToCompletion;
// it is false for both Faulted and Canceled tasks.
Console.WriteLine(
$"[after wait] Status={slowCalculation.Status}, IsCompleted={slowCalculation.IsCompleted}, IsCompletedSuccessfully={slowCalculation.IsCompletedSuccessfully}");
Console.WriteLine(
$"[after wait] IsFaulted={slowCalculation.IsFaulted}, IsCanceled={slowCalculation.IsCanceled}, Result={slowCalculation.Result}");
Console.WriteLine();
// ----------------------------------------------------------------------------
// Part 3: waiting for one or more tasks.
// ----------------------------------------------------------------------------
Console.WriteLine("--- Part 3: waiting for one or more tasks ---");
Task<string> reportA = Task.Run(() =>
{
Thread.Sleep(300);
return "Report A";
});
Task<string> reportB = Task.Run(() =>
{
Thread.Sleep(150);
return "Report B";
});
// Wait(TimeSpan) blocks only up to the given time and returns a bool that tells
// whether the task actually finished in time. 50 ms is deliberately shorter than
// reportB's 150 ms delay, so this wait is expected to time out.
bool finishedInTime = reportB.Wait(TimeSpan.FromMilliseconds(50));
Console.WriteLine($"[Wait(timeout)] Did reportB finish within 50 ms? {finishedInTime}");
// WaitAll blocks the calling thread until every listed task has completed.
Task.WaitAll(reportA, reportB);
Console.WriteLine($"[WaitAll] Both reports are ready: '{reportA.Result}', '{reportB.Result}'.");
Task<string> reportC = Task.Run(() =>
{
Thread.Sleep(300);
return "Report C";
});
Task<string> reportD = Task.Run(() =>
{
Thread.Sleep(100);
return "Report D";
});
// WaitAny blocks until the FIRST of the listed tasks completes and returns its
// index in the array. reportD is clearly the faster one (100 ms vs 300 ms), so its
// index is expected here.
int firstIndex = Task.WaitAny(reportC, reportD);
string firstValue = firstIndex == 0 ? reportC.Result : reportD.Result;
Console.WriteLine($"[WaitAny] The first finished task was at index {firstIndex} ('{firstValue}').");
Console.WriteLine();
// ----------------------------------------------------------------------------
// Part 4: combining tasks with WhenAll and WhenAny.
// ----------------------------------------------------------------------------
Console.WriteLine("--- Part 4: combining tasks with WhenAll and WhenAny ---");
Task<int> readingNorth = Task.Run(() =>
{
Thread.Sleep(200);
return 18;
});
Task<int> readingSouth = Task.Run(() =>
{
Thread.Sleep(120);
return 21;
});
Task<int> readingEast = Task.Run(() =>
{
Thread.Sleep(180);
return 19;
});
// Task.WhenAll returns ONE task that completes when ALL given tasks have completed.
// Its Result is an array with every individual result, in the same order as the
// inputs. Unlike Wait(), reading .Result here does not block for long, because the
// three readings above are already running in parallel.
int[] temperatures = Task.WhenAll(readingNorth, readingSouth, readingEast).Result;
Console.WriteLine($"[WhenAll] Collected {temperatures.Length} readings: {string.Join(", ", temperatures)}.");
Task<int> sourceA = Task.Run(() =>
{
Thread.Sleep(250);
return 101;
});
Task<int> sourceB = Task.Run(() =>
{
Thread.Sleep(90);
return 102;
});
// Task.WhenAny returns ONE task that completes as soon as the FIRST given task
// completes. Its Result is that winning task itself, not the value directly, so the
// value still has to be read through .Result on the winner.
Task<int> fastestSource = Task.WhenAny(sourceA, sourceB).Result;
Console.WriteLine($"[WhenAny] The fastest source answered with {fastestSource.Result}.");
Console.WriteLine();
// ----------------------------------------------------------------------------
// Part 5: reading the Exception property.
// ----------------------------------------------------------------------------
Console.WriteLine("--- Part 5: reading the Exception property ---");
Task<int> divideByZero = Task.Run(() =>
{
int denominator = 0;
return 10 / denominator;
});
try
{
// Wait() (just like reading .Result) rethrows the task's error wrapped in an
// AggregateException, even though only a single DivideByZeroException was
// actually thrown.
divideByZero.Wait();
}
catch (AggregateException)
{
// Swallowed on purpose: the interesting part is inspecting Exception below,
// without triggering a second throw.
}
// The Exception property lets code inspect a faulted task without throwing again.
// Flatten() removes nested AggregateException layers, and InnerException reaches
// the real error that was thrown inside the task.
Exception? realError = divideByZero.Exception?.Flatten().InnerException;
Console.WriteLine($"[Exception] Status={divideByZero.Status}, Message='{realError?.Message}'.");
Console.WriteLine();
// ----------------------------------------------------------------------------
// Part 6: cancelling a task.
// ----------------------------------------------------------------------------
Console.WriteLine("--- Part 6: cancelling a task ---");
using CancellationTokenSource cts = new();
// The token is passed as the second argument to Task.Run. The task body must check
// it by itself; passing the token alone does not stop a running loop automatically.
Task<int> cancellableCount = Task.Run(() =>
{
for (int step = 1; step <= 5; step++)
{
// ThrowIfCancellationRequested checks the token and throws
// OperationCanceledException as soon as cancellation has been requested.
cts.Token.ThrowIfCancellationRequested();
Thread.Sleep(100);
Console.WriteLine($"[cancellable task] Step {step} done.");
}
return 5;
}, cts.Token);
// Cancelling after 250 ms lets two or three steps print before the task stops,
// which makes the effect of cancellation visible without slowing the demo down.
Thread.Sleep(250);
cts.Cancel();
try
{
cancellableCount.Wait();
}
catch (AggregateException)
{
// Expected: the task observed the cancellation request and stopped itself.
}
Console.WriteLine(
$"[cancellation] Final status: {cancellableCount.Status}, IsCanceled={cancellableCount.IsCanceled}");
Console.WriteLine();
}
How It Works
Part 1 – creating tasks.
squareResult and cubeResult show the two ways to start real background work: Task.Run and Task.Factory.StartNew. Both return a Task<int> and behave the same way for this simple case.cachedResult shows that Task.FromResult never runs anything in the background; the value 42 is available the instant the task object is created.noWorkNeeded and pause demonstrate Task.CompletedTask and Task.Delay, two very different kinds of "no real work" tasks: one is already finished, the other finishes later purely based on time.Task.WaitAll is used here only to keep the console output in order before Part 2 starts.
Part 2 – status and properties.
- The status is read once before
Wait() (typically Running) and once after (RanToCompletion), so the transition is visible. IsCompletedSuccessfully is highlighted because it is the safest single check before touching Result: it is true only when the task neither faulted nor was cancelled.
Part 3 – waiting.
reportB.Wait(TimeSpan.FromMilliseconds(50)) intentionally times out, because the task needs 150 ms but only 50 ms is allowed; the returned bool is false.Task.WaitAll(reportA, reportB) then blocks until both are actually done, so both results can be read safely afterwards.Task.WaitAny(reportC, reportD) returns as soon as the faster task (reportD, 100 ms) finishes, without waiting for reportC (300 ms) at all.
Part 4 – WhenAll and WhenAny.
Task.WhenAll(...) collects three independent readings into one int[] once every one of them has completed.Task.WhenAny(...) returns the winning task, not its value, which is why fastestSource.Result is read on the result of WhenAny, one extra step compared to WhenAll.
Part 5 – exceptions.
divideByZero always throws DivideByZeroException, so Wait() throws an AggregateException, which is caught and ignored on purpose.divideByZero.Exception?.Flatten().InnerException then reaches the original exception safely, without causing a second throw.
Part 6 – cancellation.
- The task loop checks
cts.Token.ThrowIfCancellationRequested() before every step, so it can react to cancellation between steps. cts.Cancel() is called from the main thread 250 ms after the task started, which is enough time for two or three steps to print before the task observes the request and stops itself.- The final status is
Canceled, and IsCanceled is true.
Output
=== The Task class: core concept ===
--- Part 1: creating tasks ---
[Task.Run] Calculating the square of 6.
[Task.Factory.StartNew] Calculating the cube of 3.
[Task.FromResult] Cached result is 42.
--- Part 2: task status and properties ---
[before wait] Id=1, Status=Running, IsCompleted=False
[after wait] Status=RanToCompletion, IsCompleted=True, IsCompletedSuccessfully=True
[after wait] IsFaulted=False, IsCanceled=False, Result=5
--- Part 3: waiting for one or more tasks ---
[Wait(timeout)] Did reportB finish within 50 ms? False
[WaitAll] Both reports are ready: 'Report A', 'Report B'.
[WaitAny] The first finished task was at index 1 ('Report D').
--- Part 4: combining tasks with WhenAll and WhenAny ---
[WhenAll] Collected 3 readings: 18, 21, 19.
[WhenAny] The fastest source answered with 102.
--- Part 5: reading the Exception property ---
[Exception] Status=Faulted, Message='Attempted to divide by zero.'.
--- Part 6: cancelling a task ---
[cancellable task] Step 1 done.
[cancellable task] Step 2 done.
[cancellable task] Step 3 done.
[cancellation] Final status: Canceled, IsCanceled=True
The exact Id value in Part 2 depends on how many tasks were created earlier in the program run, so it may differ from the number shown here. Everything else is deterministic.
Example 2 – Real-World Scenario: A Payment Confirmation with a Timeout
The second example shows one of the most common real-world uses of Task: giving a slow operation a maximum waiting time. A checkout page asks a (simulated) bank service to confirm a payment. If the bank takes too long, the page shows a timeout message instead of waiting forever.
Code Example
/// <summary>
/// Real-world example.
/// A checkout page needs to confirm a payment with a bank service that can sometimes
/// be slow. Task.WhenAny races the real call against a Task.Delay "timeout task", so the
/// page never waits longer than the allowed time.
/// </summary>
public static void RunPaymentTimeoutScenario()
{
Console.WriteLine("=== Real-world scenario: a payment confirmation with a timeout ===");
// CheckPaymentAsync stands for a slow call to an external bank service. 1500 ms is
// used so it is clearly slower than the 800 ms timeout defined below.
Task<string> checkPayment = Task.Run(() =>
{
Thread.Sleep(1500);
return "Payment approved";
});
// The timeout itself is just another task: Task.Delay completes after the given
// time without blocking any thread while it waits. 800 ms represents the maximum
// time the checkout page is willing to wait for the bank to answer.
Task timeoutSignal = Task.Delay(800);
// Task.WhenAny races the two tasks against each other and completes as soon as the
// first one finishes. This is the standard way to add a timeout to an operation
// that does not support one natively.
Task winner = Task.WhenAny(checkPayment, timeoutSignal).Result;
if (winner == checkPayment)
{
Console.WriteLine($"Checkout result: {checkPayment.Result}");
}
else
{
Console.WriteLine("Checkout result: the bank did not answer in time, please try again.");
}
Console.WriteLine();
}
How It Works
checkPayment represents the real, potentially slow work: contacting the bank. Here it is simulated with a 1500 ms delay.timeoutSignal is an ordinary Task.Delay(800) – nothing more than a task that becomes complete after 800 ms.Task.WhenAny(checkPayment, timeoutSignal) completes as soon as either one finishes. Because the timeout (800 ms) is shorter than the payment check (1500 ms) in this run, the timeout task wins the race.- Comparing
winner by reference to checkPayment tells the code which of the two tasks actually finished first, without needing any extra flag or variable. - The slower
checkPayment task keeps running in the background even after WhenAny returns; a production version would typically pass a CancellationToken into it so it can stop early once the timeout has fired.
Output
=== Real-world scenario: a payment confirmation with a timeout ===
Checkout result: the bank did not answer in time, please try again.
This output is deterministic because the 800 ms timeout is always shorter than the 1500 ms simulated bank call, so the timeout branch always wins in this example.
When to Use
Use Task when:
- You need to run work in the background without managing a raw
Thread yourself. - You need to know when work has finished, what it produced, or whether it failed – through
Status, Result and Exception. - You need to combine several pieces of independent work, either waiting for all of them (
WhenAll/WaitAll) or reacting to the first one (WhenAny/WaitAny). - You need to add a timeout to an operation that does not support one on its own, using the
WhenAny + Delay pattern shown above.
Prefer alternatives when:
- The work is purely CPU-bound data processing over a large collection;
Parallel.For or Parallel.ForEach is usually a better fit. - You are inside an
async method; prefer await together with Task.WhenAll/Task.WhenAny over the blocking Wait(), WaitAll and Result members shown in Part 3, which can cause deadlocks in UI or ASP.NET contexts. - You need a fixed chain of steps where each step depends on the previous one's result; see the dedicated article on task continuations for
ContinueWith.
Running the Examples
Both methods are called one after another from Program.cs:
TaskFundamentalsExample.Run();
TaskFundamentalsExample.RunPaymentTimeoutScenario();