Using TaskExecutor in Spring

The asynchronous operations in the background must be executed by one entity. This is the job of an executor.


To abstract this executor, Spring has declared the data type TaskExecutor. A TaskExecutor inherits from Executor in the Java SE library.


“TaskExecutor”: The "Runnable" Executor from Spring


From today’s perspective, a TaskExecutor would not be necessary, but the Java standard library only introduced the Executor type in Java 5. The TaskExecutor from Spring is older.


Both interfaces declare method execute(Runnable), so they can execute a code block of type Runnable. Which strategy is used to execute the Runnable is up to the implementation. Often a thread pool is used, but the Runnable could also be executed by a chosen thread. This is often used in the GUI environment, for example, where a block of code is to be executed in the GUI thread. It’s all a matter of the TaskExecutor implementation.


TaskExecutor Implementations

The Spring Framework provides a number of implementations of the TaskExecutor interface.


“TaskExecutor” Implementations


Let’s turn our attention to two implementations: the ThreadPoolTaskExecutor and the ConcurrentTaskExecutor. The ThreadPoolTaskExecutor is a kind of predecessor of the current java.util.concurrent.ThreadPoolExecutor, which works internally with a pool of threads. The ThreadPoolTaskExecutor is more powerful than the ThreadPoolExecutor of Java SE because the Spring thread pool can be reconfigured at runtime. Often, a thread pool is set up early at application startup and runs as long as the application. However, usage might deviate from the planned usage pattern and initial configuration, and then it’s useful if, for example, the number of concurrent threads can be changed at runtime. The Java SE thread pool can’t do this.


Another useful data type is the ConcurrentTaskExecutor, which brings an existing Executor implementation from Java SE into the Spring universe. The ConcurrentTaskExecutor is an application of the adapter pattern, which adapts two incompatible interfaces to each other, with the core functionality being the same. Spring 6.1 introduces support for a TaskExecutor featuring virtual threads through the VirtualThreadTaskExecutor.

Declare TaskExecutor Beans and Use Them for @Async

When using asynchronous operations with the @Async annotation, a TaskExecutor can be specified for execution. To accomplish this, a Spring-managed bean must be created and named accordingly:


@Bean( "threadPoolTaskExecutor" )

public TaskExecutor myThreadPoolTaskExecutor() {

   ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();



   return executor;



@Bean( "concurrentTaskExecutor" )

public TaskExecutor myConcurrentTaskExecutor () {

   return new ConcurrentTaskExecutor( Executors.newFixedThreadPool(3) );



The name is explicitly set, but, of course, the method name would be fine too.


The first TaskExecutor uses the Spring-specific type ThreadPoolTaskExecutor, and the second uses the Java SE ThreadPoolExecutor, which is adapted into a Spring data type TaskExecutor via ConcurrentTaskExecutor.


@Async( "threadPoolTaskExecutor" ) public void abc() { }

@Async( "concurrentTaskExecutor" ) public void xyz() { }


This allows the asynchronous calls to be processed with differently configured executors.


Set Executor and Handle Exceptions

When Spring uses a TaskExecutor for execution, an exception may occur in the Runnable. The question then becomes: What happens to the exception? In a dream method, with @Async methods, even checked exceptions can be forwarded to the framework.


What should happen with the exceptions that arrive at the framework can be configured via an AsyncConfigurer. The interface looks like this.


package org.springframework.scheduling.annotation;


import …


public interface AsyncConfigurer {



   default Executor getAsyncExecutor() { return null; }



   default AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {

       return null;




The AsyncConfigurer provides two objects: an Executor that executes the code, and an AsyncUnchaughtExceptionHandler for the uncaught exceptions. The AsyncUncaughtExceptionHandler data type also comes from the Spring environment.


package org.springframework.aop.interceptor;


import java.lang.reflect.Method;



public interface AsyncUncaughtExceptionHandler {


void handleUncaughtException(Throwable ex, Method method,

                             Object... params);



In Java SE, there is something similar called UncaughtExceptionHandler. The difference between the two is that in the Java SE environment, an UncaughtExceptionHandler is used only for unchecked exceptions because everything else has already been caught with Runnable according to the API contract (the run() method of Runnable has no throws). Conversely, in the Spring environment, the AsyncUncaughtExceptionHandler can handle all exceptions. The default implementation of Spring is the SimpleAsyncUncaughtExceptionHandler.


public class SimpleAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler {



   private static final Log logger =





   public void handleUncaughtException(Throwable ex, Method method,

                                       Object... params) {

       if (logger.isErrorEnabled()) {


               "Unexpected exception occurred invoking async method: "

               + method, ex);





From the implementation, you can see that the exception is logged and nothing else happens.

A Custom AsyncConfigurer Implementation

If a custom executor and an AsyncUncaughtExceptionHandler need to be specified, an AsyncConfigurer implementation can be defined as a @Configuration, which might look like this:



class AsyncConfig implements AsyncConfigurer {

   private final Logger log = LoggerFactory.getLogger( getClass() );



   public Executor getAsyncExecutor() {

       ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

       // …

       return executor;




   public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {

       return ( throwable, method, params ) -> {

  "Exception: {}", throwable );

  "Method: {}", method );

           IntStream.range( 0, params.length ).forEach(

               index -> "Parameter {}: {}", index, params[ index ] )






The AsyncConfigurer implementation overrides both default methods. For the Executor, the getAsyncExecutor() method builds a ThreadPoolTaskExecutor. getAsyncUncaughtExceptionHandler() returns an implementation of the AsyncUncaughtExceptionHandler functional interface via a lambda expression. The data received from the parameter list (Throwable ex, Method method, Object... params) are all logged.


Editor’s note: This post has been adapted from a section of the book Spring Boot 3 and Spring Framework 6 by Christian Ullenboom.


Spring Boot 3 and Spring Framework 6
Spring Boot 3 and Spring Framework 6

Say goodbye to dependencies, bogged-down code, and inflexibility! With the Spring framework and Spring Boot, you’ll painlessly create Java applications that are production ready. Start with the basics: containers for Spring-managed beans, Spring framework modules, and proxies. Then learn to connect to relational databases, implement Jakarta Persistence, use Spring Data JPA, and work with NoSQL databases. Get the right know-how for modern software development with Spring and Java!

Learn More
Rheinwerk Computing
by Rheinwerk Computing

Rheinwerk Computing is an imprint of Rheinwerk Publishing and publishes books by leading experts in the fields of programming, administration, security, analytics, and more.