31
@Override
@Async
public void asyncExceptionTest() {
    int i=1/0;
}

How can I log this using Spring Async framework without having to put try catch around every async method? It doesn't seem to pass to the DefaultUncaughtExceptionHandler like normal.

Andrii Abramov
  • 10,019
  • 9
  • 74
  • 96
DD.
  • 21,498
  • 52
  • 157
  • 246

4 Answers4

24

@Async methods can be configured with a custom Executor to log any thrown exceptions.

The following code implements this pattern. Any method tagged with @Async will use the Executor returned by the method public Executor getAsyncExecutor(). This returns the HandlingExecutor which takes care of all logging (in this case it just prints the word "CAUGHT!" but you can replace with logging.

@Configuration
@EnableAsync
public class ExampleConfig implements AsyncConfigurer {
    @Bean
    public Runnable testExec() {
        return new TestExec();
    }

    @Override
    public Executor getAsyncExecutor() {
        final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(7);
        executor.setMaxPoolSize(42);
        executor.setQueueCapacity(11);
        executor.setThreadNamePrefix("MyExecutor-");
        executor.initialize();
        return new HandlingExecutor(executor);
    }
}

public class HandlingExecutor implements AsyncTaskExecutor {
    private AsyncTaskExecutor executor;

    public HandlingExecutor(AsyncTaskExecutor executor) {
        this.executor = executor;
    }

    @Override
    public void execute(Runnable task) {
        executor.execute(task);
    }

    @Override
    public void execute(Runnable task, long startTimeout) {
        executor.execute(createWrappedRunnable(task), startTimeout);
    }

    @Override
    public Future<?> submit(Runnable task) {
        return executor.submit(createWrappedRunnable(task));
    }

    @Override
    public <T> Future<T> submit(final Callable<T> task) {
        return executor.submit(createCallable(task));
    }

    private <T> Callable<T> createCallable(final Callable<T> task) {
        return new Callable<T>() {
            @Override
            public T call() throws Exception {
                try {
                    return task.call();
                } catch (Exception e) {
                    handle(e);
                    throw e;
                }
            }
        };
    }

    private Runnable createWrappedRunnable(final Runnable task) {
        return new Runnable() {
            @Override
            public void run() {
                try {
                    task.run();
                } catch (Exception e) {
                    handle(e);
                }
            }
        };
    }

    private void handle(Exception e) {
        System.out.println("CAUGHT!");
    }
}
M. Justin
  • 14,487
  • 7
  • 91
  • 130
DD.
  • 21,498
  • 52
  • 157
  • 246
  • 1
    Could your provide an explanation of how the code above addresses the problem, please? – Jon Oct 02 '13 at 08:34
  • Any method tagged with @Async will use the Executor returned by the method "public Executor getAsyncExecutor()". This returns the "HandlingExecutor" which takes care of all logging (in this case it just prints the word "CAUGHT!" but you can replace with logging. – DD. Oct 02 '13 at 22:39
  • That makes sense. Two more questions: 1) What is the testExec function and TestExec object for? 2) How do I go about using this code in my own application? – Jon Oct 03 '13 at 07:50
  • Old post, but `createWrappedRunnable()` should catch `RuntimeException` and re-throw it after handling to be consistent. – Matt Byrne Jul 09 '14 at 20:34
  • @MattByrne To be consistent with what exactly? – DD. Jul 10 '14 at 00:55
  • To be consistent with the original behaviour of the `Runnable` supplied and also to be consistent with the `createCallable` method. You've essentially swallowed the exception which isn't great practice - layers above could react accordingly and handle the exception as well if they desire. It was a very useful post by the way and I've used your code - just thought I'd give feedback on something I noticed. – Matt Byrne Jul 10 '14 at 04:58
  • You are setting up an executor at application level here. What if you need two or more different threadpool executors, and configure uncaughtexception handler for each one of them? How can I do that? – de_xtr Jan 04 '18 at 08:16
  • This doesn't work for `@Async` methods having `Future` return type. `AsyncConfigurer's getAsyncUncaughtExceptionHandler()` combined with `TaskDecorator`s looks better choice to me. – masT May 18 '19 at 13:43
23

Update: Since Spring 4.1

Since Spring 4.1 It is possible to have an AsyncUncaughtExceptionHandler for @Async void methods.

Spring Reference Doc, Chapter 34.4.5 Exception management with @Async

... With a void return type however, the exception is uncaught and cannot be transmitted. For those cases, an AsyncUncaughtExceptionHandler can be provided to handle such exceptions.

By default, the exception is simply logged. A custom AsyncUncaughtExceptionHandler can be defined via AsyncConfigurer or the task:annotation-driven XML element.

(This feature was introduced after DD raised an impovement request: https://jira.spring.io/browse/SPR-8995 , see comments of this answer)


Before Spring 4.1

Looks like an missing feature how to handle exceptions of an void returning @Async Method. (I can not find any hint in the reference or java doc)

What I can imagine of an solution: Try to use AspectJ to write some kind of wrapper arround all @Async methods that log the exceptions.

For the log term, I would recommend to create an freature request in the spring bug tracker.

Community
  • 1
  • 1
Ralph
  • 118,862
  • 56
  • 287
  • 383
  • 2
    Cheers..I've raised this as an improvement: https://jira.springsource.org/browse/SPR-8995 – DD. Jan 05 '12 at 11:03
  • 1
    Please vote for that SPR-8995 above. I ran several times into unexplainable behaviour because of those unlogged exceptions. – Markus Malkusch Nov 17 '13 at 08:32
  • 2
    Nice ... looks like https://jira.spring.io/browse/SPR-8995 is resolved and will ship with 4.1 (it's in RC1 now). – Matt Byrne Jul 10 '14 at 20:44
20

First off all, you should create a custom exception handler class like following;

@Component
public class AsyncExceptionHandler implements AsyncUncaughtExceptionHandler {

        private final Logger logger = LoggerFactory.getLogger(AsyncExceptionHandler.class);

        @Override
        public void handleUncaughtException(Throwable ex, Method method, Object... params) {
            logger.error("Unexpected asynchronous exception at : "
                    + method.getDeclaringClass().getName() + "." + method.getName(), ex);
        }

    }

After that, you should set your customized exception handler class in your configuration like following;

@Configuration
@EnableAsync
public class AsyncConfig extends AsyncConfigurerSupport {

    @Autowired
    private AsyncExceptionHandler asyncExceptionHandler;

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return asyncExceptionHandler;
    }

}

Note : Injectable exception handler is an option. You can create a new instance for every exception. My advice is using Injection for exception handler class, because spring's default scope is singleton so there is no need to create new instance for every exception.

  • Thank you @Daria , i edited. There is no difference between Autowired and Inject in this sample as you know. Same responsibility, but for Spring, we should use, Autowired . – Mustafa Onur AYDIN Nov 08 '17 at 13:51
  • Do NOT forget to add `@Lazy` to the `@Autowired` annotation, otherwise you will run into subtle bugs because some injected beans will not be CGLIB enhanced. See: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/scheduling/annotation/EnableAsync.html – Werner Altewischer Mar 08 '22 at 15:35
2

You can use standard Spring AOP approach

@Aspect
@Component
@Slf4j
public class AsyncHandler {

   @Around("@annotation(org.springframework.scheduling.annotation.Async)")
   private Object handle(ProceedingJoinPoint pjp) throws Throwable {
       try {
           Object retVal = pjp.proceed();
           return retVal;
       } catch (Throwable e) {
           log.error("in ASYNC, method: " + pjp.getSignature().toLongString() + ", args: " + AppStringUtils.transformToWellFormattedJsonString(pjp.getArgs()) + ", exception: "+ e, e);
           throw e;
       }
   }

}
panser
  • 1,949
  • 22
  • 16