C# / .NET • Tasks & async
C# Tasks — Simple Guide with Examples

Understand Task in easy words: run work now or later, wait for it to finish, get a result, handle errors, cancel, and combine many tasks.

Quick Summary

  • Task = a promise that some work will finish in the future.
  • async/await = easy keywords to wait for a Task without blocking the thread.
  • CPU work → use Task.Run. I/O work (file, db, web) → use the API's built‑in async methods.
  • Parallel = many things at the same time. Async = don't block while waiting. They can be used together or separately.

1) Task Basics

What is a Task? A Task represents work that is happening or will happen. You can await it to get the result when it's done.

  • Task → no value, just completion.
  • Task<T> → returns a value of type T.
  • ValueTask → like Task, with less allocation in certain high‑perf cases.
Easy rule: Library code may use ValueTask. App code can use Task 99% of the time.

Minimal Example

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        Task work = Task.Delay(1000);   // pretend work (1 sec)
        await work;                     // wait without blocking
        Console.WriteLine("Done!");
    }
}

2) Create & Start Tasks

Prefer the Task.Run pattern for CPU-bound work. For I/O, use the API's own Async method (like ReadAsync, GetAsync).

CPU-bound (use Task.Run)

using System;
using System.Linq;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        // Heavy calculation on a background thread
        int sum = await Task.Run(() => Enumerable.Range(1, 10_000_000).Sum());
        Console.WriteLine($"Sum = {sum}");
    }
}

I/O-bound (use native async APIs)

using System;
using System.Net.Http;
using System.Threading.Tasks;

class Program
{
    static readonly HttpClient http = new HttpClient();

    static async Task Main()
    {
        string html = await http.GetStringAsync("https://example.com");
        Console.WriteLine($"Downloaded {html.Length} chars");
    }
}

3) Returning Values

Use Task<T> to return a value. Await it to get the value.

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        int answer = await ComputeAsync(21);
        Console.WriteLine($"Answer is {answer}");
    }

    static async Task<int> ComputeAsync(int x)
    {
        await Task.Delay(300); // simulate work
        return x * 2;
    }
}

4) async / await (the easy way to wait)

using System;
using System.IO;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        await File.WriteAllTextAsync("out.txt", "Hello async!");
        string text = await File.ReadAllTextAsync("out.txt");
        Console.WriteLine(text);
    }
}

5) Combine Tasks: WhenAll / WhenAny

Run in parallel and wait for all

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        Task t1 = Task.Delay(500);
        Task t2 = Task.Delay(800);
        Task t3 = Task.Delay(200);

        await Task.WhenAll(t1, t2, t3);
        Console.WriteLine("All finished!");
    }
}

Wait for the first one

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        Task t1 = Task.Delay(500);
        Task t2 = Task.Delay(800);

        Task first = await Task.WhenAny(t1, t2);
        Console.WriteLine("The first task finished!");
    }
}

6) Cancellation

Use CancellationToken to stop work politely.

using System;
using System.Threading;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        using var cts = new CancellationTokenSource();
        var token = cts.Token;

        var task = DoWorkAsync(token);
        cts.CancelAfter(500); // cancel after 0.5s

        try
        {
            await task;
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine("Canceled!");
        }
    }

    static async Task DoWorkAsync(CancellationToken token)
    {
        for (int i = 0; i < 10; i++)
        {
            token.ThrowIfCancellationRequested();
            await Task.Delay(200, token);
            Console.WriteLine($"Step {i}");
        }
    }
}

7) Error Handling

Exceptions inside a Task are re-thrown when you await it. Wrap with try/catch.

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        try
        {
            await MightFailAsync();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Oops: {ex.Message}");
        }
    }

    static async Task MightFailAsync()
    {
        await Task.Delay(200);
        throw new InvalidOperationException("Something went wrong");
    }
}
Multiple tasks failing? With Task.WhenAll, exceptions are wrapped in AggregateException. Catch it and inspect InnerExceptions.

8) Task vs Thread (easy view)

Use Task for most app work, especially when you need to wait for I/O without blocking.
Use Thread only for special cases (thread affinity, STA, long-running dedicated worker).

9) Common Pitfalls

10) Practice Snippets

Download many URLs in parallel (WhenAll)

using System;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;

class Program
{
    static readonly HttpClient http = new HttpClient();

    static async Task Main()
    {
        string[] urls = {
            "https://example.com",
            "https://www.microsoft.com",
            "https://learn.microsoft.com"
        };

        Task<int>[] jobs = urls.Select(async u =>
        {
            string s = await http.GetStringAsync(u);
            return s.Length;
        }).ToArray();

        int[] sizes = await Task.WhenAll(jobs);
        Console.WriteLine(string.Join(", ", sizes));
    }
}

Progress reporting

using System;
using System.Threading;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        var progress = new Progress<int>(p => Console.WriteLine($"{p}%"));
        await DoWorkAsync(progress);
    }

    static async Task DoWorkAsync(IProgress<int> progress)
    {
        for (int i = 1; i <= 5; i++)
        {
            await Task.Delay(250);
            progress.Report(i * 20);
        }
    }
}

Timeout with CancellationToken

using System;
using System.Threading;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1));
        try
        {
            await SlowAsync(cts.Token);
            Console.WriteLine("Completed");
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine("Timed out");
        }
    }

    static async Task SlowAsync(CancellationToken token)
    {
        await Task.Delay(3000, token);
    }
}