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?
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();
}
}