Doing Some Cleanup in a BackgroundService

Download full source code.

There is something a little strange about the Worker Service template and its BackgroundService in .NET.

You can create a new Worker Service project using -

`dotnet new worker -n MyWorker

The Problem

The Worker class uses a BackgroundService as its base class. The Worker has an ExecuteAsync(...) method that is called when the service is started, and it takes a CancellationToken as a parameter.

So far, so good.

There is a while loop in the ExecuteAsync(...) method that checks if the CancellationToken is canceled. If it is canceled, the loop exits, and anything outside the loop should be executed.

Still ok.

There is a Task.Delay(1000, stoppingToken) call in the loop that delays for 1 second and takes a CancellationToken as a parameter.

My Worker does very little, so the delay during each iteration of the loop is long compared to the time the actual work takes. Any cancellation is likely to happen during the delay period.

If the CancellationToken is canceled during the delay, the Task.Delay(...) should throw a TaskCanceledException.

But it does not.

The ExecuteAsync(...) method stops running entirely, but no exception is thrown.

Any code outside the while loop is not executed.

This means that any cleanup code I want to run when the BackgroundService is canceled will likely not run because cancellation usually happens during the delay.

Why does this happen?

I don’t know.

If I run a simple console application with a loop around Task.Delay(1000, stoppingToken) call and cancel the CancellationToken, the Task.Delay(...) throws a TaskCanceledException and I see the exception.

 1CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(3000);
 2CancellationToken stoppingToken = cancellationTokenSource.Token;
 3
 4int count = 0;
 5while(!stoppingToken.IsCancellationRequested)
 6{
 7 Console.WriteLine($"{++count} - {DateTime.Now:HH:mm:ss}");
 8 await Task.Delay(1000, stoppingToken);
 9}
10
11Console.WriteLine("Exiting");

The output of this will be

1 - 15:33:27
2 - 15:33:28
3 - 15:33:29
Unhandled exception. System.Threading.Tasks.TaskCanceledException: A task was canceled.
 at Program.<Main>$(String[] args) in /home/bryan/dev/blog/2025/TaskDelayWithCancellation/Program.cs:line 8
 at Program.<Main>(String[] args)

The TaskCanceledException is thrown as you would expect.

But the BackgroundService does not throw the exception!

The Solution

The solution is to put a try...catch block around the call to Task.Delay(1000, stoppingToken), catching the TaskCanceledException.

In my use case, I want to do some cleanup when the BackgroundService is canceled. To make sure this happens I need to immediately exit the while loop and run a few more lines of code.

Here is the updated Worker class -

 1protected override async Task ExecuteAsync(CancellationToken stoppingToken)
 2{
 3 while (!stoppingToken.IsCancellationRequested)
 4 {
 5 Console.WriteLine($"Worker running at: {DateTime.Now:HH:mm:ss}");
 6 //await Task.Delay(1000, stoppingToken); // does not throw TaskCanceledException and won't execute the cleanup code
 7
 8 try 
 9 {
10 await Task.Delay(1000, stoppingToken);
11 }
12 catch (TaskCanceledException)
13 {
14 Console.WriteLine($"Cancelled during the Task.Delay(...).");
15 break;
16 }
17 Console.WriteLine($"Still in loop running at: {DateTime.Now:HH:mm:ss}");
18
19 }
20 Console.WriteLine("Cancellation requested, doing some cleanup.");
21}

From what I can see the explicit try...catch block is the best way to immediately cancel the Task.Delay(1000, stoppingToken) and exit the while loop.

If you go with other options like Task.Delay(1000), you need to wait until the delay is over before the while loop exits. But also, any code after the delay will still run.

If you try things like -

1await Task.Delay(1000, stoppingToken).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing)
2// or 
3await Task.Delay(1000, stoppingToken).ContinueWith(t => { return t.Exception == default;});

They won’t exit the while loop either, again running code that comes after the delay.

Download full source code.

comments powered by Disqus

Related