Search code examples
javaspringspring-bootasynchronous

Handling exceptions thrown by Spring @Async methods


In my Spring Boot app, I have a method that sends emails by making (blocking) HTTP calls to the SendGrid API. In some cases I call this method several times per request, so in order to mitigate the impact on the request thread, I call this method asynchronously by annotating it with @Async.

public class EmailService {

    @Async
    public void send(String address, String subject, String body) {
        // make a HTTP call to SendGrid's API
    }
}

public class PasswordService {

    @Autowired
    private EmailService emailService;

    public void resetAll(String emailAddresses) {
        emailAddresses.forEach(email -> {
            emailService.send(email, "Your new password is", "open-sesame");   
        });
    }
}

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public void handleException(Exception ex) {
        ex.printStackTrace();
    }
}

The problem with this approach is that it bypasses the application's exception handling. If an exception is thrown by emailService.send, the global exception handler is not called, and therefore a 500 HTTP status is not returned to the client.

If I remove @Async, the global exception handlers is called as expected. How can I call this method asynchronously, without bypassing the global exception handler?


Solution

  • Based on the comments, it seems I didn't do a great job of explaining the problem. I've tested the solution below and am satisfied that it meets my requirements

    public class EmailService {
    
        @Async
        public CompletableFuture<?> send(String address, String subject, String body) {
            try {
                // make a HTTP call to SendGrid's API
                return CompletableFuture.completedFuture(null);
            } catch (Exception ex) {
                return CompletableFuture.failedFuture(ex);
            }
        }
    }
    
    public class PasswordService {
    
        @Autowired
        private EmailService emailService;
    
        public void resetAll(String emailAddresses) {
    
            var emailTasks = emailAddresses.stream()
                .map(email -> emailService.send(email, "Your new password is", "open-sesame"))
                .toArray(CompletableFuture[]::new);
    
            // wait for all the calls to emailService to complete, throw an
            // exception if any of them fail
            CompletableFuture.allOf(emailTasks).join();
        }
    }