C#
asynchronous programming
async/await
BackgroundWorker
refactoring

Refactoring Backgroundworker to async/await

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

BackgroundWorker was a common way to keep desktop applications responsive before task-based async became standard in .NET. Today, async and await usually produce simpler code, better cancellation support, and cleaner exception handling. Refactoring is not just syntax cleanup; it is a shift from event-driven threading to task-based asynchronous flow.

Why BackgroundWorker Feels Heavy

BackgroundWorker splits one operation across several event handlers:

  • 'DoWork'
  • 'ProgressChanged'
  • 'RunWorkerCompleted'

That works, but it scatters the logic and makes data flow harder to follow. Error handling is also awkward because exceptions must cross event boundaries instead of being awaited naturally.

A typical legacy pattern looks like this:

csharp
1private BackgroundWorker worker = new BackgroundWorker();
2
3public Form1()
4{
5    InitializeComponent();
6    worker.DoWork += Worker_DoWork;
7    worker.RunWorkerCompleted += Worker_RunWorkerCompleted;
8}
9
10private void startButton_Click(object sender, EventArgs e)
11{
12    worker.RunWorkerAsync();
13}
14
15private void Worker_DoWork(object sender, DoWorkEventArgs e)
16{
17    e.Result = LongRunningOperation();
18}
19
20private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
21{
22    resultLabel.Text = e.Result?.ToString();
23}

This is functional, but the operation is fragmented.

The async and await Replacement

With task-based async, the long-running method becomes a task-returning method, and the UI event handler awaits it directly:

csharp
1private async void startButton_Click(object sender, EventArgs e)
2{
3    startButton.Enabled = false;
4
5    try
6    {
7        string result = await LongRunningOperationAsync();
8        resultLabel.Text = result;
9    }
10    catch (Exception ex)
11    {
12        MessageBox.Show(ex.Message);
13    }
14    finally
15    {
16        startButton.Enabled = true;
17    }
18}
19
20private async Task<string> LongRunningOperationAsync()
21{
22    await Task.Delay(2000);
23    return "Completed";
24}

This version keeps the UI responsive without creating manual event choreography. The code reads top to bottom, which is the main benefit of the refactor.

Map Old Concepts to New Ones

When converting BackgroundWorker, translate each concern explicitly:

  • background execution becomes Task or Task<T>
  • completion callback becomes await
  • progress reporting becomes IProgress<T>
  • cancellation becomes CancellationToken

Here is a progress and cancellation example:

csharp
1private CancellationTokenSource? _cts;
2
3private async void startButton_Click(object sender, EventArgs e)
4{
5    _cts = new CancellationTokenSource();
6    var progress = new Progress<int>(value => progressBar.Value = value);
7
8    try
9    {
10        await DownloadAsync(progress, _cts.Token);
11        statusLabel.Text = "Done";
12    }
13    catch (OperationCanceledException)
14    {
15        statusLabel.Text = "Canceled";
16    }
17}
18
19private async Task DownloadAsync(IProgress<int> progress, CancellationToken token)
20{
21    for (int i = 1; i <= 10; i++)
22    {
23        token.ThrowIfCancellationRequested();
24        await Task.Delay(200, token);
25        progress.Report(i * 10);
26    }
27}

That is the direct modern equivalent of WorkerReportsProgress and WorkerSupportsCancellation, but with much better composability.

CPU-Bound Versus I/O-Bound Work

Do not mechanically replace everything with Task.Run. If the work is I/O-bound, such as HTTP, database, or file access, prefer native async APIs and await them directly.

Use Task.Run mainly when:

  • the code is CPU-heavy
  • the API is synchronous and cannot be changed yet
  • you are in a desktop UI and need to move blocking work off the UI thread

That distinction matters because fake async built from Task.Run is not the same as true non-blocking I/O.

Refactoring Strategy

The safest migration path is incremental:

  1. extract the long-running logic into a separate method
  2. convert that method to Task or Task<T>
  3. replace completion handlers with await
  4. add CancellationToken and IProgress<T> only where needed

This avoids rewriting the entire screen at once.

Common Pitfalls

The most common mistake is blocking on async code with .Result or .Wait(). In UI applications, that can deadlock or freeze the interface.

Another mistake is using async void everywhere. Only UI event handlers should usually be async void; reusable methods should return Task.

A third issue is wrapping already-async APIs in Task.Run, which adds thread-pool overhead without solving a real problem.

Summary

  • 'BackgroundWorker can usually be replaced with task-based async code.'
  • 'await removes the need for separate completion handlers.'
  • Use IProgress<T> for progress and CancellationToken for cancellation.
  • Prefer native async APIs for I/O-bound work and reserve Task.Run for CPU-bound work.
  • Refactor incrementally so behavior stays stable while the code becomes easier to maintain.

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.