BackgroundWorker
exceptions
error handling
.NET
C#

Unhandled exceptions in BackgroundWorker

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

BackgroundWorker does not treat exceptions the same way in every event. An exception thrown in DoWork is captured and surfaced later through RunWorkerCompletedEventArgs.Error. An exception thrown in ProgressChanged or RunWorkerCompleted, however, occurs on the UI thread and can still crash the application if unhandled.

How BackgroundWorker Flows Exceptions

A typical BackgroundWorker setup looks like this:

csharp
1using System;
2using System.ComponentModel;
3
4var worker = new BackgroundWorker();
5
6worker.DoWork += (sender, e) =>
7{
8    throw new InvalidOperationException("Something failed in DoWork");
9};
10
11worker.RunWorkerCompleted += (sender, e) =>
12{
13    if (e.Error != null)
14    {
15        Console.WriteLine(e.Error.Message);
16    }
17};
18
19worker.RunWorkerAsync();
20Console.ReadLine();

The key point is that the exception from DoWork does not need to be caught inside the worker just to prevent process termination. BackgroundWorker captures it and exposes it through e.Error.

Check RunWorkerCompletedEventArgs.Error

This is the standard place to handle worker-thread failure.

csharp
1worker.RunWorkerCompleted += (sender, e) =>
2{
3    if (e.Error != null)
4    {
5        Console.WriteLine("Background operation failed: " + e.Error.Message);
6        return;
7    }
8
9    if (e.Cancelled)
10    {
11        Console.WriteLine("Operation was cancelled");
12        return;
13    }
14
15    Console.WriteLine("Success: " + e.Result);
16};

This is the BackgroundWorker equivalent of checking whether an async task faulted.

Exceptions in ProgressChanged Are Different

ProgressChanged runs on the thread that created the worker, which is usually the UI thread in WinForms or WPF-style usage. If you throw there, it is not wrapped into e.Error.

csharp
1worker.WorkerReportsProgress = true;
2
3worker.ProgressChanged += (sender, e) =>
4{
5    throw new Exception("UI-side failure");
6};

That exception behaves like any other unhandled UI-thread exception and can crash the app if not caught.

Same for RunWorkerCompleted

The completion event also runs on the original synchronization context, commonly the UI thread. So this is dangerous:

csharp
1worker.RunWorkerCompleted += (sender, e) =>
2{
3    throw new Exception("Completion handler failure");
4};

The exception is no longer a background exception at that point. It is now a normal UI-thread exception.

If You Catch in DoWork, Propagate Deliberately

Sometimes you want to add context before surfacing an error.

csharp
1worker.DoWork += (sender, e) =>
2{
3    try
4    {
5        // background operation
6        throw new InvalidOperationException("Original cause");
7    }
8    catch (Exception ex)
9    {
10        throw new ApplicationException("Failed while importing file", ex);
11    }
12};

This is useful when the raw exception message is too low-level to be meaningful in the UI or logs.

Cancellation Is Not an Exception

Do not model normal cancellation as an exception if the worker supports cancellation.

csharp
1worker.WorkerSupportsCancellation = true;
2
3worker.DoWork += (sender, e) =>
4{
5    if (worker.CancellationPending)
6    {
7        e.Cancel = true;
8        return;
9    }
10};

Then handle it cleanly in RunWorkerCompleted via e.Cancelled.

Logging and Diagnostics

At minimum, log:

  • the top-level exception message
  • the full stack trace
  • the inner exception if you wrapped it

Swallowing e.Error silently makes background failures much harder to debug because the UI may simply stop updating without any visible clue about what went wrong.

Modern Note

BackgroundWorker still appears in older desktop code, but newer .NET code often prefers Task, async, and await. If you are designing a fresh application, those newer abstractions give clearer exception flow and composition. But if you are maintaining existing BackgroundWorker code, understanding where exceptions land is still essential.

Common Pitfalls

The biggest mistake is assuming every exception connected to BackgroundWorker will appear in RunWorkerCompletedEventArgs.Error. Only exceptions from DoWork are captured that way. Another is swallowing background exceptions without logging them. Developers also often throw from RunWorkerCompleted or ProgressChanged and then wonder why the app still crashes. Finally, treating cancellation as an exception makes control flow harder to reason about than it needs to be.

Summary

  • Exceptions thrown in DoWork are captured and exposed through RunWorkerCompletedEventArgs.Error.
  • Exceptions thrown in ProgressChanged or RunWorkerCompleted happen on the UI thread and can still crash the app.
  • Check e.Error and e.Cancelled explicitly in RunWorkerCompleted.
  • Wrap background exceptions only when you want to add useful context.
  • Prefer Task-based async code for new .NET work, but handle BackgroundWorker exceptions correctly in legacy code.

Course illustration
Course illustration

All Rights Reserved.