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.