Skip to content

Microsoft.Extensions.Hosting: Application stop throws exception if host is still starting #111013

Open

Description

@kmcclellan

Description

If the application is stopped while one or more hosted services are still starting, a misleading TaskCanceledException is logged and bubbles up to Program. This is often due to an exception thrown in BackgroundService, in which case it obscures the real problem.

Reproduction Steps

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var builder = new HostApplicationBuilder();
builder.Services.AddHostedService<ServiceThatStops>();
builder.Services.AddHostedService<ServiceThatWaits>();

using var host = builder.Build();

try
{
    host.Run();
}
catch (Exception exception)
{
    Console.WriteLine($"'Run()' threw exception: {exception}");
}

class ServiceThatStops : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await Task.Delay(10, stoppingToken);
        throw new Exception("My environment is bad! This is the exception I want to see.");
    }
}

class ServiceThatWaits : IHostedService
{
    public Task StartAsync(CancellationToken cancellationToken)
    {
        return Task.Delay(100, cancellationToken);
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        return Task.CompletedTask;
    }
}

Expected behavior

Only the triggering exception should be logged. Cancellation of the remaining service startup should be handled by the host without throwing or logging an error.

Actual behavior

fail: Microsoft.Extensions.Hosting.Internal.Host[9]
      BackgroundService failed
      System.Exception: My environment is bad! This is the exception I want to see.
         at ServiceThatStops.ExecuteAsync(CancellationToken stoppingToken) in C:\Users\KMcClellan\Desktop\HelloHosting\Program.cs:line 22
         at Microsoft.Extensions.Hosting.Internal.Host.TryExecuteBackgroundServiceAsync(BackgroundService backgroundService)
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. To avoid this behavior, configure this to Ignore; however the BackgroundService will not be restarted.
      System.Exception: My environment is bad! This is the exception I want to see.
         at ServiceThatStops.ExecuteAsync(CancellationToken stoppingToken) in C:\Users\KMcClellan\Desktop\HelloHosting\Program.cs:line 22
         at Microsoft.Extensions.Hosting.Internal.Host.TryExecuteBackgroundServiceAsync(BackgroundService backgroundService)
info: Microsoft.Hosting.Lifetime[0]
      Application is shutting down...
fail: Microsoft.Extensions.Hosting.Internal.Host[11]
      Hosting failed to start
      System.Threading.Tasks.TaskCanceledException: A task was canceled.
         at Microsoft.Extensions.Hosting.Internal.Host.<StartAsync>b__14_1(IHostedService service, CancellationToken token)
         at Microsoft.Extensions.Hosting.Internal.Host.ForeachService[T](IEnumerable`1 services, CancellationToken token, Boolean concurrent, Boolean abortOnFirstException, List`1 exceptions, Func`3 operation)
'Run()' threw exception: System.Threading.Tasks.TaskCanceledException: A task was canceled.
   at Microsoft.Extensions.Hosting.Internal.Host.<StartAsync>b__14_1(IHostedService service, CancellationToken token)
   at Microsoft.Extensions.Hosting.Internal.Host.ForeachService[T](IEnumerable`1 services, CancellationToken token, Boolean concurrent, Boolean abortOnFirstException, List`1 exceptions, Func`3 operation)
   at Microsoft.Extensions.Hosting.Internal.Host.<StartAsync>g__LogAndRethrow|14_3(<>c__DisplayClass14_0&)
   at Microsoft.Extensions.Hosting.Internal.Host.StartAsync(CancellationToken cancellationToken)
   at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token)
   at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token)
   at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.Run(IHost host)
   at Program.<Main>$(String[] args) in C:\Users\KMcClellan\Desktop\HelloHosting\Program.cs:line 10

Regression?

No response

Known Workarounds

Catch OperationCanceledException in any implementation of StartAsync(...) instead of throwing to acknowledge.

Configuration

  • .NET runtime 9.0
  • .NET SDK 9.0.101
  • Microsoft.Extensions.Hosting 9.0.0

Other information

Similar to #98935.

Activity

  1. steveharter commented on Jan 10, 2025

    @steveharter
    Contributor

    In the repro, there are two HostedServices instances and they share a cancellation token to ensure they are cancelled together. They also have a guarantee that they all have a chance to start and thus that causes TaskCancelledException across the other threads\services.

    Since the sync portion of each service is run sequentially before they are all awaited via Task.Run(), any validation logic before an await is run and should get the result you are looking for. i.e. in the repro, don't have the await Task.Delay(10, stoppingToken) before the validation logic.

    However, a likely better alternative is to use IStartupValidator service which runs before Start():

    ...
    services.AddSingleton<IStartupValidator, ValidationService>();
    ...
    
    class ValidationService : IStartupValidator
    {
        public void Validate()
        {
            throw new Exception("My environment is bad! This is the exception I want to see.");
        }
    }

    or the newer IHostedLifecycleService.StartingAsync which can be implemented on the service:

    class ServiceThatStops : BackgroundService, IHostedLifecycleService
    {
    ...
        public Task StartingAsync(CancellationToken cancellationToken) => throw new Exception("My environment is bad!!!!!!!!!!!!!!! This is the exception I want to see.");
        public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
        public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
        public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
  2. added
    needs-further-triageIssue has been initially triaged, but needs deeper consideration or reconsideration
    and removed
    untriagedNew issue has not been triaged by the area owner
    on Jan 10, 2025
  3. kmcclellan commented on Jan 10, 2025

    @kmcclellan
    Author

    Sure, throwing before the await does not reproduce the issue. But it's not the case that all exceptions will be thrown in the synchronous section of the implementation. In my case, it is a failed HTTP response on startup that indicates the bad environment.

    You can't convince me that throwing TaskCanceledException from IHost.Run() with no cancellation token passed is correct behavior.

  4. steveharter commented on Jan 13, 2025

    @steveharter
    Contributor

    In my case, it is a failed HTTP response on startup that indicates the bad environment.

    The TaskCanceledException is actually from ServiceThatWaits.StartAsync() when it calls Task.Delay() which throws here due to the shared cancellation token:

    Change StartAsync() to this to see that you can have a graceful shutdown without TaskCanceledException:

        public async Task StartAsync(CancellationToken cancellationToken)
        {
            while (cancellationToken.IsCancellationRequested == false)
            {
            }
        }
  5. added
    needs-author-actionAn issue or pull request that requires more info or actions from the author.
    on Jan 13, 2025
  6. kmcclellan commented on Jan 14, 2025

    @kmcclellan
    Author

    The TaskCanceledException is actually from ServiceThatWaits.StartAsync() when it calls Task.Delay() which throws here due to the shared cancellation token:

    Yes, I'm aware. Throwing TaskCanceledException (or any OperationCanceledException) in a method that accepts a cancellation token is known as "acknowledging" cancellation. This is what implementers of StartAsync(...) should do if the token is triggered and I am not done with my startup work. The host should not log this as an error, nor allow the exception to bubble up to Program (unless Program passed a cancellation token - not the case in my repro).

    Again, this is very similar to #98935, where it's granted that BackgroundService.ExecuteAsync(...) should be able to acknowledge cancellation without this representing an error condition. The same should be true for other members and other types of hosted service.

  7. steveharter commented on Jan 15, 2025

    @steveharter
    Contributor

    Again, this is very similar to #98935, where it's granted that BackgroundService.ExecuteAsync(...) should be able to acknowledge cancellation without this representing an error condition. The same should be true for other members and other types of hosted service.

    The difference is that both #98935 and the original #56032 have a single hosted service.

    To not throw TCE for other hosted services when a background service is still starting (which is itself also a hosted service) could be considered a breaking change.

    Also, since there can be more than one background service we would have to define the rules around that such as "if any background service fails during startup, then ignore TCE for all other hosted services including other background services". I believe this can be done simply by checking if the host is shutting down already like the other two issues linked above.

    You can't convince me that throwing TaskCanceledException from IHost.Run() with no cancellation token passed is correct behavior.

    It should act the same with or without a specified cancellation token since one is created if not passed to host.RunAsync(). In general, and as mentioned before, the shared cancellation token is created in order to support stopping all threads\hosted services in a timely manner:

    using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _applicationLifetime.ApplicationStopping))

    So the TCE exception will still occur, but it can be ignored. I put up a prototype at https://github.com/steveharter/runtime/tree/Issue-98935 to do that.

    @eerhardt @halter73 any thoughts here?

  8. kmcclellan commented on Jan 17, 2025

    @kmcclellan
    Author

    We still seem to be talking past each other :/ To be clear, the host is correct to trigger cancellation for all services if any of them throws an exception. What is not correct is the logging of acknowledgement TCEs as errors and allowing them to bubble up higher in the call stack than where the token source is created.

    Granted, this is a fairly minor issue. But fairly often someone complains to me that an app is crashing and sends me the exception details for the TCE, and I have to explain to this bug to them and that they need to look for the actual error earlier in the logs.

  9. added this to the 10.0.0 milestone on Mar 7, 2025
  10. added
    enhancementProduct code improvement that does NOT require public API changes/additions
    and removed
    needs-further-triageIssue has been initially triaged, but needs deeper consideration or reconsideration
    on Mar 7, 2025
  11. modified the milestones: 10.0.0, Future on Jul 26, 2025
  12. karelz commented on Jan 28, 2026

    @karelz
    Member

    Triage: Application stop during host starting is edge case. We might consider not logging cancellation exceptions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-Extensions-HostingenhancementProduct code improvement that does NOT require public API changes/additions

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions