CodeGym /Courses /C# SELF /Classes Task and <...

Classes Task and Task<TResult>

C# SELF
Level 60 , Lesson 0
Available

1. Introduction

In the world of asynchronous programming we live by the principle of "delegation with feedback". Imagine: you need to download a huge file, analyze gigabytes of logs, or send a request to a distant server. Instead of freezing and waiting like a statue, we tell the system: "Handle this task, and I'll work on something else. When you're done — be sure to let me know!"

This is where Task comes in — an elegant embodiment of that approach. It's not just a technical abstraction, it's a kind of "smart middleman" that takes responsibility for doing the work and ensures the result doesn't get lost in the digital void.

Task works like a personal assistant you assign an important job to. It nods, writes the task in its notebook and says: "Go on with your stuff, I'll definitely find you when everything is ready." And it really does — with either the result in hand or a honest explanation of what went wrong.

If you look for a more everyday analogy, Task is like a modern appointment booking system: you register online for a doctor's visit, get a confirmation, and you don't need to spend hours in a waiting room. The system will remind you about the upcoming appointment, and meanwhile you can live your life.

Class Task

Task is the basic building block of asynchronous programming in .NET. It represents a running or future operation whose result will be available later. If a method shouldn't return anything, we just use Task.

public async Task BackupToCloudAsync()
{
    // Does backup magic, returns nothing
}

Class Task<TResult>

If you need to return a result (for example, a string, number, object…), use Task<TResult>:

public async Task<string> DownloadHtmlAsync(string url)
{
    // Downloads a page and returns the HTML
    return "<html>...</html>";
}

Why Task and not Thread?

Thread manages the actual thread (that's heavy and risky), while Task is a higher-level abstraction: it can run on the thread pool, can operate asynchronously without allocating a new thread (for example, for I/O operations) and doesn't force you to care about low-level details.

The Task class lets you express: "I want to run this action", and how exactly it's executed — let .NET decide!

2. Anatomy of a Task object

Properties and methods of Task you should know

Property / Method Description
Status
Current state of the task
Result
Result for Task<TResult> (blocks the thread)
IsCompleted
Whether the task is completed
IsFaulted
Whether the task threw an exception
IsCanceled
Whether the task was canceled
Wait()
Blocks the current thread until completion (dangerous)
ContinueWith()
Run another task after completion
Exception
Access to the exception if the Task finished with an error
Id
Unique identifier of the task

How an async method with Task works

sequenceDiagram
    participant Main as Main Thread
    participant Task as Task (Background job)
    Main->>Task: Start Task.Run(() => ...)
    Note right of Task: Running in background
(CPU or I/O) alt Task completed Task->>Main: await completed, continue else Error Task->>Main: await throws exception end

3. Creating and running tasks: how Task works

Async methods with async

The most common case — you just declare the method as async and return either Task or Task<TResult> (as we just saw).

Task.Run: execution on the thread pool

If you need to run some heavy work in the background (for example, calculate big numbers or encode video), you can use Task.Run:

Task work = Task.Run(() =>
{
    // Heavy calculations — don't block the main thread!
    Console.WriteLine("Background calculations started...");
    Thread.Sleep(2000); // Simulate long work
    Console.WriteLine("Background calculations finished!");
});

If it's useful to get a result:

Task<int> calculateTask = Task.Run(() =>
{
    // For example, sum of first 100 numbers
    int sum = 0;
    for (int i = 1; i <= 100; i++) sum += i;
    return sum;
});

Task.Factory.StartNew

This is a lower-level and more flexible way that allows fine-tuning task start (for example, specifying a scheduler, passing parameters, etc.). In modern code it's almost always recommended to use Task.Run because it's simpler and safer.

4. App of the day: our book catalog

Suppose we have a book catalog app and we need to add a function to load books from a "cloud" source — this will be an I/O-bound operation (slow HTTP request or file read).

Let's add a method that asynchronously "loads" books (we'll emulate delay):

public class Book
{
    public string Title { get; set; }
    public string Author { get; set; }
}

public class BookCatalog
{
    public List<Book> Books { get; set; } = new();

    public async Task LoadBooksAsync()
    {
        Console.WriteLine("Loading books...");
        await Task.Delay(2000); // Simulate long load (e.g. HTTP or file)
        Books = new List<Book>
        {
            new Book { Title = "CLR via C#", Author = "Jeffrey Richter" },
            new Book { Title = "C# in Depth", Author = "Jon Skeet" }
        };
        Console.WriteLine("Books loaded successfully.");
    }
}

In Main call the async load (using await):

var catalog = new BookCatalog();
await catalog.LoadBooksAsync();
Console.WriteLine($"The catalog has {catalog.Books.Count} books.");

Table: Main ways to create and start a Task

Creation method How used Result Use case
async-method
async Task / async Task<T>
Asynchronous operation Usually I/O, convenience
Task.Run
Task.Run(() => { ... })
Background job CPU-bound (calculations)
TaskCompletionSource<T>
Create and complete the Task manually Full control for the programmer Rare, for low-level stuff

5. Lifecycle of a Task

Task can be in different states:

  • Created — task is created but not started (for Tasks that require explicit start).
  • WaitingToRun — waiting in the thread pool queue.
  • Running — running.
  • WaitingForActivation — waiting to be started or external activation.
  • RanToCompletion — completed successfully.
  • Faulted — finished with an error (exception).
  • Canceled — canceled (if cancellation is supported).

Diagram

flowchart LR
    Start -->|Start task| Running
    Running -->|Success| Completed
    Running -->|Error| Faulted
    Running -->|Cancel| Canceled

Let's check this in practice

Task task = Task.Run(() =>
{
    Thread.Sleep(1000);
});
Console.WriteLine(task.Status); // Usually: Running or WaitingToRun
await task;
Console.WriteLine(task.Status); // RanToCompletion after finish

6. How to get result from Task<TResult>?

Task<TResult> is a wrapper around a result that will appear in the future. When you need to wait for the result, use await:

Task<int> sumTask = Task.Run(() =>
{
    int sum = 0;
    for (int i = 1; i <= 5; i++) sum += i;
    return sum;
});

int result = await sumTask;
Console.WriteLine(result); // 15

If you forget to write await, you'll get a Task (a promise), not the result. That's a typical "async trap".

Alternative: synchronous result retrieval (DON'T DO IN UI!)

Sometimes (e.g. in tests) you need to get the result without await. You can use the .Result property:

int result = sumTask.Result;

But if the Task hasn't finished yet, this code blocks the thread, and if that's the UI thread the app will freeze! So: prefer await always.

Common mistakes with Task and Task<TResult>

Forgot to return Task, the method became void. If a method has no return value — return Task, not void, otherwise you can't handle errors.

Ignoring await. You just call the method without waiting and the task lives on its own ("fire and forget"). You'll no longer know when it finished or if it failed.

Blocking wait via .Result or .Wait(). It's easy to get a deadlock, especially in UI and ASP.NET. Use await only.

7. Advanced capabilities of Task

Task chaining: ContinueWith

You can "attach" actions to run after a task completes using ContinueWith:

Task.Run(() => 10)
    .ContinueWith(t =>
    {
        Console.WriteLine($"Done! Result: {t.Result}");
    });

But in modern C# people usually do this with async/await — it's easier to read.

Example: Parallel and sequential data loading

Say you need to load two books from different sources. You can start both Tasks in parallel and wait for both:

public async Task LoadBooksFromMultipleSourcesAsync()
{
    Task<List<Book>> t1 = LoadFromCloudAsync();
    Task<List<Book>> t2 = LoadFromLocalAsync();

    // Wait for both tasks in parallel
    await Task.WhenAll(t1, t2);

    // Combine results
    Books = t1.Result.Concat(t2.Result).ToList();
}

private async Task<List<Book>> LoadFromCloudAsync()
{
    await Task.Delay(2000); // "Cloud"
    return new List<Book> { new Book { Title = "Cloud Book", Author = "Cloud Author" } };
}

private async Task<List<Book>> LoadFromLocalAsync()
{
    await Task.Delay(1000); // "Local disk"
    return new List<Book> { new Book { Title = "Local Book", Author = "Local Author" } };
}

Note: using await Task.WhenAll(...) both requests start at the same time and run in parallel (if possible), and the app waits for both to finish.

8. Useful nuances

Task and Fire-and-forget

Sometimes you want to fire a task and not wait for it to finish (for example, send logs to the cloud or "toast" while the user works):

async void LogToCloudAsync(string message)
{
    await Task.Run(() =>
    {
        // Long log send
        Thread.Sleep(1000);
        Console.WriteLine($"Log sent: {message}");
    });
}

But remember: if an error happens in such a task — it's hard to find out. So if possible, return Task and at least log exceptions inside!

Task and Task<TResult> in real life

  • In client UWP/WPF/WinForms apps don't block the UI — use Task for long operations (files, network).
  • In WebAPI/ASP.NET Task helps not wasting threads waiting for network/DB, improving throughput.
  • Organize "parallel" execution: download, process and save concurrently.
  • Almost all long methods have Async variants: File.ReadAllTextAsync, HttpClient.GetStringAsync and others.

FAQ and unexpected moments

Question: Why does Task sometimes run synchronously?
Answer: If the operation is already completed (e.g. result cached), the compiler and/or scheduler may complete the method on the same thread synchronously. That's normal and speeds up repeated calls.

Question: Why shouldn't you use async void?
Answer: Such a method can't be awaited, you can't catch its errors or track completion. Use Task, and reserve async void only for EventHandler (e.g. Button_Click).

Question: Can you start multiple tasks and wait for only one?
Answer: Yes — use Task.WhenAny.

2
Task
C# SELF, level 60, lesson 0
Locked
Using Task to Return a Result
Using Task to Return a Result
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION