5

How can I make the controllerAdvice class catch the exception that is thrown from completablefutrue. In the code below I have a method checkId that throws a checked exception. I call this method using completablefuture and wrap the checked exception inside CompletionException. Although I have a handler method in controller advice class, but it didn't handle the error.

package com.example.demo.controller;
@RestController
public class HomeController {

    @GetMapping(path = "/check")
    public CompletableFuture<String> check(@RequestParam("id") int id) {
        return CompletableFuture.supplyAsync(() -> {
            try {
                return checkId(id);
            }
            catch (Exception e) {
                throw new CompletionException(e);
            }
        });
    }

    public String checkId(int id) throws Exception  {
        if (id < 0) {
            throw new MyException("Id must be greater than 0");
        }
        return "id is good";
    }

}

-

package com.example.demo;
public class MyException extends Exception {

    public MyException(String message) {
        super(message);
    }

}

-

package com.example.demo;
@ControllerAdvice
public class ExceptionResolver {

    @ExceptionHandler(value = CompletionException.class)
    public String handleCompletionException(CompletionException ex) {
        return ex.getMessage();
    }

}

--

package com.example.demo;
@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}

Stack trace:

com.example.demo.MyException: Id must be greater than 0
    at com.example.demo.controller.HomeController.checkId(HomeController.java:30) ~[classes/:na]
    at com.example.demo.controller.HomeController.lambda$0(HomeController.java:19) ~[classes/:na]
    at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1700) ~[na:na]
    at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.exec(CompletableFuture.java:1692) ~[na:na]
    at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:290) ~[na:na]
    at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1020) ~[na:na]
    at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1656) ~[na:na]
    at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1594) ~[na:na]
    at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:177) ~[na:na]

2020-01-24 14:38:30.489 ERROR 938 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is com.example.demo.MyException: Id must be greater than 0] with root cause

com.example.demo.MyException: Id must be greater than 0
    at com.example.demo.controller.HomeController.checkId(HomeController.java:30) ~[classes/:na]
    at com.example.demo.controller.HomeController.lambda$0(HomeController.java:19) ~[classes/:na]
    at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1700) ~[na:na]
    at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.exec(CompletableFuture.java:1692) ~[na:na]
    at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:290) ~[na:na]
    at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1020) ~[na:na]
    at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1656) ~[na:na]
    at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1594) ~[na:na]
    at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:177) ~[na:na]
MA1
  • 926
  • 10
  • 28
  • can you show the packages structure as well as error message with stack trace – Ryuzaki L Jan 24 '20 at 20:17
  • @Deadpool. Added the package structure. – MA1 Jan 24 '20 at 20:27
  • can you add main class as well error message with stack trace @MoA – Ryuzaki L Jan 24 '20 at 20:28
  • Added. @Deadpool – MA1 Jan 24 '20 at 20:40
  • actually change the `@ExceptionHandler` to `@ExceptionHandler(value = MyException.class)` and try @MoA – Ryuzaki L Jan 24 '20 at 20:43
  • Didn't work. @Deadpool – MA1 Jan 24 '20 at 20:49
  • also see this https://stackoverflow.com/questions/43124391/restcontrolleradvice-vs-controlleradvice – Ryuzaki L Jan 24 '20 at 20:53
  • Unfortunately, @RestControllerAdvice didn't work as well. The controller advice class works for other endpoints, but for the exception that is thrown from the completablefuture, it didn't get handled by the controller advice class. It looks like completablefuture wraps the exception. – MA1 Jan 24 '20 at 20:59
  • 1
    @Deadpool. your previous comment helped. I just needed to change the method parameter as well. Here are the changes: `@ExceptionHandler(value = MyException.class) public String handleCompletionException(MyException ex) { return ex.getMessage(); }` Wow, completablefuture unwrap the exception. That's why it didn't work! first! – MA1 Jan 24 '20 at 21:03

1 Answers1

5

@Deadpool and I were able to identify the problem.

Inside the catch block in the completableFuture code, the exception is wrapped inside CompletionException exception which is a run-time exception. When the endpoint gets hit, at some point the CompletionException exception gets unwrapped to the original exception which is of type MyException (the checked exception). Which means the handler method should handle an exception of type MyException instead of CompletionException.

@ExceptionHandler(value = MyException.class)
@ResponseBody 
public String handleCompletionException(MyException ex) { 
     return ex.getMessage(); 
}
MA1
  • 926
  • 10
  • 28