akuma8
akuma8

Reputation: 4691

Get exception thrown within annotated @Async method in @RestControllerAdvice

There is a quite similar question here, but the answer does not suffice for my question.

I have this method in a @Service class:

@Async
public void activateUser(...){
  if(someCondition){
   throw new GeneralSecurityException();
  }
}

The controller:

@GetMapping( "/activate")
public ResponseEntity<Void> activate(...){
    myService.activateUser(...);
}

And the controller advice:

@RestControllerAdvice( basePackages = "com.app.security" )
public class SecurityAdviceController extends ResponseEntityExceptionHandler {

     @ExceptionHandler( GeneralSecurityException.class )
     public ResponseEntity<GeneralSecurityExceptionBody> handleGeneralSecurityException( GeneralSecurityException ex ) {
     return ResponseEntity
            .status( HttpStatus.MOVED_PERMANENTLY )
            .header( HttpHeaders.LOCATION, ex.getUrl() )
            .body( null );
}

Here we are. Since the exception will be thrown in another thread, how can I proceed to make it available for @RestControllerAdvice?

Many suggest to implement AsyncUncaughtExceptionHandler, and I agree, but that does not answer the question.

When I remove @Async, everything is fine, and I can see that the same thread does all the tasks, but with @Async I have 2 threads involved.

One solution would be to get the exception thrown by the parent thread (but it's too cumbersome, and I don't see how to implement that).

Thanks for your help.

Upvotes: 2

Views: 1876

Answers (1)

Oleksii Zghurskyi
Oleksii Zghurskyi

Reputation: 4365

If you really want to work asynchronous, then most likely you are using wrong tools - better to switch to Spring WebFlux and use reactive approach instead.

Going back to the question, I can suggest 2 approaches:

  • Get rid of @Async or use SyncTaskExecutor, so the task will be executed synchronously in the calling thread.
  • Get rid of @ExceptionHandler(GeneralSecurityException.class) for this particular method. Instead, use CompletableFuture and provide exceptionally handled logic. Below is a sketch of using CompletableFuture in controller and service:
@Controller
public class ApiController {
    private final Service service;
    public ApiController(Service service) {
        this.service = service;
    }
    @GetMapping( "/activate")
    public CompletableFuture<Void> activate(...){
        return service.activateUser(...)
               .exceptionally(throwable -> ... exception handling goes here ...)
    }
}

@Service
public class Service {
    @Async
    public CompletableFuture<Void> activateUser(...) {
        CompletableFuture<Void> future = new CompletableFuture<>();
        ... your code goes here ...
        if(someCondition){
           future.completeExceptionally(new GeneralSecurityException());
        } else {
           future.complete(null);
        }
        return future;
    }
}

Upvotes: 1

Related Questions