Progress Reporting

Vaibhav • September 11, 2025

In the previous article, we explored ConfigureAwait-a subtle but powerful tool that controls where your asynchronous code resumes after an await. Now that we understand how async methods behave behind the scenes, it’s time to make them more interactive and user-friendly. One common requirement in real-world applications is to show progress while an operation is running. Whether you're downloading a file, processing a large dataset, or performing a long-running calculation, users appreciate feedback. That’s where progress reporting comes in.

In this article, we’ll explore how to report progress from asynchronous methods using the IProgress<T> interface. We’ll learn how it works, how to integrate it into your async workflows, and how to keep your UI responsive while showing meaningful updates. We’ll also cover best practices, common mistakes, and how progress reporting fits into the broader async programming model.

Why Progress Reporting Matters

Imagine a user clicks a button to start a long-running task-like uploading a video or scanning a directory. If nothing happens for several seconds, they might think the app is frozen or broken. But if you show a progress bar, percentage, or status message, they know the app is working and can estimate how long it will take.

Progress reporting improves user experience, builds trust, and makes your application feel responsive-even when the actual work takes time. In asynchronous programming, it’s especially important because tasks run in the background and don’t block the UI.

Introducing IProgress<T>

.NET provides a built-in interface for reporting progress: IProgress<T>. It’s designed to work seamlessly with async methods and ensures that progress updates are marshaled back to the original synchronization context-like the UI thread.

Here’s a simple example:


async Task DownloadAsync(IProgress<int> progress)
{
    for (int i = 0; i <= 100; i += 10)
    {
        await Task.Delay(200); // simulate work
        progress?.Report(i);
    }
}
    

This method simulates a download by waiting in a loop and reporting progress every 10%. The Report method sends the progress value to the caller, which can display it in the UI.

Using Progress in UI Applications

In a UI app like WPF or WinForms, you can use Progress<T> to receive updates on the UI thread. This lets you update controls safely without worrying about cross-thread exceptions.


async void StartButton_Click(object sender, EventArgs e)
{
    var progress = new Progress<int>(value =>
    {
        progressBar.Value = value;
        statusLabel.Text = $"Progress: {value}%";
    });

    await DownloadAsync(progress);
}
    

This code creates a Progress<int> object with a lambda that updates the UI. When DownloadAsync calls Report, the lambda runs on the UI thread, keeping the interface responsive and safe.

Always use Progress<T> in UI apps. It captures the synchronization context and ensures that updates run on the correct thread.

Custom Progress Types

You’re not limited to integers. You can define custom types to report richer progress information-like status messages, percentages, or intermediate results.


class ProgressInfo
{
    public int Percent { get; set; }
    public string Message { get; set; }
}

async Task ProcessAsync(IProgress<ProgressInfo> progress)
{
    for (int i = 0; i <= 100; i += 20)
    {
        await Task.Delay(300);
        progress?.Report(new ProgressInfo
        {
            Percent = i,
            Message = $"Step {i / 20 + 1} completed"
        });
    }
}
    

This method reports both percentage and a message. The caller can display them in different UI elements or log them for diagnostics.

Progress in Console Applications

In console apps, you don’t have a synchronization context. That means Progress<T> behaves like ConfigureAwait(false)-it runs the callback on a thread pool thread. You can still use it to print progress to the console.


var progress = new Progress<int>(value =>
{
    Console.WriteLine($"Progress: {value}%");
});

await DownloadAsync(progress);
    

This works fine in console apps, but be careful if you mix progress reporting with other thread-sensitive operations.

Progress with Cancellation

You can combine progress reporting with cancellation tokens to make your async methods even more responsive. Just pass both parameters and check the token periodically.


async Task ScanAsync(IProgress<int> progress, CancellationToken token)
{
    for (int i = 0; i <= 100; i += 10)
    {
        token.ThrowIfCancellationRequested();
        await Task.Delay(200, token);
        progress?.Report(i);
    }
}
    

This method reports progress and exits early if cancellation is requested. It’s ideal for user-triggered operations like scanning or syncing.

Progress in Parallel Tasks

If you run multiple tasks in parallel, you can report progress from each one. Just make sure the progress handler is thread-safe or uses Progress<T> to marshal updates correctly.


ConcurrentBag<int> results = new ConcurrentBag<int>();
var progress = new Progress<int>(value => Console.WriteLine($"Task {value} done"));

List<Task> tasks = new List<Task>();

for (int i = 0; i < 5; i++)
{
    int id = i;
    tasks.Add(Task.Run(() =>
    {
        Thread.Sleep(500);
        results.Add(id * 10);
        progress.Report(id);
    }));
}

await Task.WhenAll(tasks);
    

Each task reports its ID when done. The progress handler prints it to the console. This pattern is useful for batch processing or distributed workloads.

Common Mistakes

One common mistake is updating the UI directly from a background thread. This causes exceptions like InvalidOperationException: Cross-thread operation not valid. Always use Progress<T> to marshal updates to the UI thread.

Another mistake is forgetting to check for null before calling Report. If the caller doesn’t provide a progress handler, calling Report throws a NullReferenceException. Use the null-conditional operator (?.) to avoid this.

Also, don’t report progress too frequently. Updating the UI every millisecond can cause flickering and performance issues. Use sensible intervals-like every 5% or every 100 items.

You can use IProgress<T> with any type-not just numbers. You can report strings, objects, or even custom classes to convey rich progress information.

Summary

Progress reporting is a key part of building responsive and user-friendly applications. It lets you show feedback during long-running operations and keeps users informed. You’ve learned how to use IProgress<T> and Progress<T> to report progress from async methods, how to integrate it into UI and console apps, and how to combine it with cancellation and parallel tasks.

In the next article, we’ll explore Async Streams, a powerful feature that lets you consume asynchronous data as a stream using await foreach. You’ll learn how to build producers and consumers that work seamlessly with async workflows.