Here's a bug that lived in .NET for over four years. If your BackgroundService threw an exception after its first await, your host would catch it, log a critical message, and then exit cleanly with exit code 0.
So everyone would think it terminated successfully. That got fixed!
The Problem
Consider this worker:
public class Worker : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);
throw new Exception("something went wrong");
}
}
Before .NET 11, the output looked like this:
crit: Microsoft.Extensions.Hosting.Internal.Host[10]
The HostOptions.BackgroundServiceExceptionBehavior is configured to StopHost.
A BackgroundService has thrown an unhandled exception, and the IHost instance is stopping.
...
System.Exception: something went wrong
info: Microsoft.Hosting.Lifetime[0]
Application is shutting down...
Process finished with exit code 0. ? !!
It get's logged as critical error, but the exit code is still 0.
Why did it behave like this?
This is one of those cases where the bug lives at the boundary of sync and async. Everything depends on whether the exception is thrown before or after the first await.
**Before the first await😗*
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
throw new Exception(); // no await yet
}
The exception propagates synchronously during host startup and crashes the process — exit code 134. Correct behavior.
After the first await
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await Task.Delay(1000, stoppingToken); // crosses the boundary
throw new Exception();
}
By this point, the host has already started. The exception gets caught by TryExecuteBackgroundServiceAsync deep inside the hosting infrastructure, logged as critical, and then the host initiates a graceful shutdown. Graceful shutdown = exit code 0.
The root cause is that host.RunAsync() returns Task (not Task<int>), and there was no mechanism for a failing background service to signal that the process should exit with an error code. It just... didn't.
The fix in .NET 11
PR #124863 makes IHost.RunAsync() and IHost.StopAsync() propagate the BackgroundService exception rather than swallowing it. Now you get:
crit: Microsoft.Extensions.Hosting.Internal.Host[10]
...
System.Exception: something went wrong
Unhandled exception. System.Exception: something went wrong
at Worker.ExecuteAsync(...)
Process finished with exit code 134. (or 1 on Windows)
What if you want the old behavior?
You can opt out by setting the exception behavior to Ignore:
services.Configure<HostOptions>(options =>
{
options.BackgroundServiceExceptionBehavior = BackgroundServiceExceptionBehavior.Ignore;
});
Ressources
The original issue: https://github.com/dotnet/runtime/issues/67146