When using virtual threads all @Scheduled executors stop working after first exception on any @Scheduled executor and no error is logged. With disabled virtual threads @Scheduled executors continue working and exception is logged with TaskUtils$LoggingErrorHandler and SimpleAsyncUncaughtExceptionHandler.

Demo is here: https://github.com/ztomic/scheduling-demo

In this demo DemoEvent is generated every 5 seconds with @Scheduled task, and there are two listeners, one is @Async and other is synchronous and both of them are throwing exception on every second invocation. Also, there is one @Scheduled task which throws exception on every second invocation.

With disabled virtual threads, all @Scheduled tasks continue working after exception.

Run application with virtual threads disabled and after that with virtual threads enabled in application.yml.

Comment From: ztomic

Also ThreadPoolTaskSchedulerCustomizer is not used with virtual threads.

@Bean
    public ThreadPoolTaskSchedulerCustomizer threadPoolTaskSchedulerCustomizer() {
        return taskScheduler -> {
            taskScheduler.setErrorHandler(t -> LoggerFactory.getLogger(ThreadPoolTaskScheduler.class).error("Unexpected error occurred in {}scheduled task.", Thread.currentThread().isVirtual() ? "virtual " : "", t));
        };
    }

Comment From: wilkinsona

When using virtual threads all @Scheduled executors stop working after first exception on any @Scheduled executor and no error is logged. With disabled virtual threads @Scheduled executors continue working and exception is logged with TaskUtils$LoggingErrorHandler and SimpleAsyncUncaughtExceptionHandler

Thanks for the report and sample project. The difference in behaviour is due to differences in Spring Framework's ThreadPoolTaskScheduler (used with platform threads) and SimpleAsyncTaskScheduler (used with virtual threads). The latter does not have an ErrorHandler and therefore does not decorate the scheduled tasks with the error handler.

I don't think there's anything we can do about this in Spring Boot as there's no way that I am aware of for us to combine the simple non-pooling behaviour of SimpleAsyncTaskScheduler with the error handling behaviour of ThreadPoolTaskScheduler. We'll transfer this issue to the Framework team for their consideration.

ThreadPoolTaskSchedulerCustomizer is not used with virtual threads

This is to be expected as there's no ThreadPoolTaskScheduler and no pooling in general when using virtual threads.

Comment From: mhalbritter

Also ThreadPoolTaskSchedulerCustomizer is not used with virtual threads.

If you use virtual threads, SimpleAsyncTaskScheduler is doing the scheduling. You can customize the scheduler with SimpleAsyncTaskSchedulerCustomizer.

If you use platform threads, ThreadPoolTaskScheduler is doing the scheduling. You can customize the scheduler with ThreadPoolTaskSchedulerCustomizer.

Comment From: ztomic

Ok, thanks for clarifications. Maybe it should be mentioned in release notes or documentation (if not already), so users are aware that if they enable virtual threads they should also handle all exceptions in @Async and @Scheduled methods to avoid this behaviour.

Comment From: jhoeller

I suppose this problem specifically materializes with fixed-delay tasks which we need to execute on the main scheduler thread... and which we therefore need to wrap in a defensive error handler.

Comment From: ztomic

I have tested with 6.1.2-SNAPSHOT and it seems that now is working ok.

Comment From: jhoeller

Good to hear! Thanks for testing the snapshot.

Comment From: kSzajo

Also ThreadPoolTaskSchedulerCustomizer is not used with virtual threads.

If you use virtual threads, SimpleAsyncTaskScheduler is doing the scheduling. You can customize the scheduler with SimpleAsyncTaskSchedulerCustomizer.

If you use platform threads, ThreadPoolTaskScheduler is doing the scheduling. You can customize the scheduler with ThreadPoolTaskSchedulerCustomizer.

ThreadPoolTaskSchedulerCustomizerdoes not contain setErrorHandler

Is it possible to declare exception handler for SimpleAsyncTaskScheduler?