Task and Task<T>

A Task is an object standing for work that will finish later; Task<T> also carries the value that work will produce.

Syntaxpublic static Task<TResult> Run<TResult>(Func<TResult> function)

A Task is an object that stands for work which has started and will finish later. It is the receipt, not the work. You hold the receipt and collect the result when it is ready.

A plain Task means work with no value to hand back. Task<T> means work that will produce a value of type T, so a Task<int> ends with an int.

How a task ends

A task ends in one of three ways. It ran to completion, it faulted because code inside it threw an exception, or it was canceled. The Status property reports which. A faulted task stores its exception instead of throwing on the spot; the exception reaches you when something awaits the task or reads Result.

A Task is not a thread

A thread executes code. A Task is bookkeeping. Most tasks in a web service finish without any thread being dedicated to them, because they were only waiting on a socket or a file. Task.Run is the case where a thread really is used: it queues work onto the thread pool, the set of worker threads .NET keeps ready, and returns a Task for that work.

Ready-made tasks

Task.FromResult(7) returns a Task<int> that is already finished with the value 7. Task.CompletedTask is the already-finished Task with no value. Both matter when a method has to return a Task but has nothing to wait for.

Example

Example · csharp
// A task that is finished the moment it exists. No waiting happens.
Task<int> ready = Task.FromResult(7);
Console.WriteLine($"ready completed before await: {ready.IsCompleted}");
Console.WriteLine($"ready value: {await ready}");

// Task.Run queues the work onto a thread pool thread and hands back a
// Task<int> that will carry the return value of the lambda.
Task<int> computed = Task.Run(() => Sum(1, 100));
Console.WriteLine($"computed value: {await computed}");

// A plain Task carries no value. Awaiting it just means "wait until done".
Task waiting = Task.Delay(50);
await waiting;
Console.WriteLine($"waiting status: {waiting.Status}");

static int Sum(int from, int to)
{
    int total = 0;

    for (int i = from; i <= to; i++)
    {
        total += i;
    }

    return total;
}

// Output:
// ready completed before await: True
// ready value: 7
// computed value: 5050
// waiting status: RanToCompletion

When to use it

  • Holding a started operation in a variable so other work can run before you need the result.
  • Pushing a long calculation onto a thread pool thread with Task.Run and collecting the answer later.
  • Writing a test double that must return Task but answers from memory.

More examples

A faulted task holds its exception

This is the single most surprising thing about tasks: failure is data until somebody asks for it. Reach for this shape when you are debugging work that seems to vanish without an error, and note the difference between reading Exception, which gives you a wrapper, and awaiting, which gives you the real exception.

Example · csharp
// The method throws, but the throw does not travel yet. It is captured
// in the Task, which moves to the Faulted state.
Task<int> failing = FailAsync();

await Task.Delay(50);                     // give it time to fail
Console.WriteLine($"status: {failing.Status}");
Console.WriteLine($"faulted: {failing.IsFaulted}");

// Exception is an AggregateException wrapper. Awaiting the task instead
// throws the original exception, which is almost always what you want.
Console.WriteLine($"stored: {failing.Exception!.InnerException!.Message}");

try
{
    await failing;
}
catch (InvalidOperationException ex)
{
    Console.WriteLine($"caught: {ex.Message}");
}

static async Task<int> FailAsync()
{
    await Task.Delay(10);
    throw new InvalidOperationException("no connection");
}

// Output:
// status: Faulted
// faulted: True
// stored: no connection
// caught: no connection

Returning a finished task without async

Interfaces often force a Task-returning signature even on an implementation that has nothing to wait for, such as an in-memory fake used in tests. Task.FromResult and Task.CompletedTask satisfy the signature without the cost of an async state machine, and the compiler will not warn you about an async method with no await.

Example · csharp
public sealed class SettingsCache
{
    private readonly Dictionary<string, string> _settings;

    public SettingsCache(Dictionary<string, string> settings)
    {
        _settings = settings;
    }

    // The answer is already in memory, so there is nothing to await.
    // Task.FromResult wraps the value in a finished Task<string>.
    // This is the longhand of what "async Task<string>" would produce
    // for a method whose body never awaits, without the state machine.
    public Task<string> GetAsync(string key)
    {
        return Task.FromResult(_settings[key]);
    }

    // No value to return, so the finished plain Task is enough.
    public Task SaveAsync(string key, string value)
    {
        _settings[key] = value;
        return Task.CompletedTask;
    }
}

Discussion

  • Be the first to comment on this lesson.