Search code examples
c#.net-core.net-8.0

why does BackgroundService not ignore exceptions depending on how they are handled


The BackgroundService in a .NET 8 Application shuts down when unhandled exceptions occur, despite configuring BackgroundServiceExceptionBehavior to Ignore.

var builder = WebApplication.CreateBuilder(args);
// ...
builder.Services.Configure<HostOptions>(o => o.BackgroundServiceExceptionBehavior = BackgroundServiceExceptionBehavior.Ignore);

public class MyBackgroundService : BackgroundService
{
    private Timer _timer;
    
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _timer = new Timer(async _ => await DoWork(stoppingToken), null, TimeSpan.Zero, Timeout.InfiniteTimeSpan);
    }

    private async Task DoWork(CancellationToken cancellationToken)
    {
        try
        {
            // Perform some work in parallel
            await Parallel.ForEachAsync(items, async item =>
            {
                // this has to be wrapped in a try catch block 
                // for the service not to shut down
                await MethodA(item);
            });
        }
        catch (Exception ex) when (ex is SomeException)
        {
            //perform actions
        }
    }

    private async Task MethodA(object item)
    {
        throw new Exception();
    }
}

The overriden ExecuteAsync method calls DoWork which uses Parallel.ForEachAsync. If an unhandled exception arises within the loop, the service crashes unless a try-catch block is included inside the Parallel.ForEachAsync method.

What do i have to consider with Exceptions regarding this?


Solution

  • There's one really big problem with your code: the exception you're throwing is not thrown in the context of your service. Your service instead creates a timer that will run your code when it eventually ticks, then immediately returns. And the timer will bubble up exceptions as normal.

    There's another problem with your code: even if you fixed the above, the exception you're throwing is thrown synchronously, or in other words, before the first await. Any synchronous exceptions get rethrown unconditionally:

                await ForeachService(_hostedServices, token, concurrent, abortOnFirstException, exceptions,
                    async (service, token) =>
                    {
                        await service.StartAsync(token).ConfigureAwait(false);
    
                        if (service is BackgroundService backgroundService)
                        {
                            _ = TryExecuteBackgroundServiceAsync(backgroundService);
                        }
                    }).ConfigureAwait(false);
    
                // Exceptions in StartAsync cause startup to be aborted.
                LogAndRethrow();
    

    Asynchronous exceptions however reach the following code, where if you use the Ignore configuration above, the host will ignore them and keep running (note that the condition is reversed, the code actually checks for StopHost and runs the stop logic then):

                _logger.BackgroundServiceFaulted(ex);
                if (_options.BackgroundServiceExceptionBehavior == BackgroundServiceExceptionBehavior.StopHost)
                {
                    _logger.BackgroundServiceStoppingHost(ex);
    
                    // This catches all exceptions and does not re-throw.
                    _applicationLifetime.StopApplication();
                }