This article is a follow-up on my previous 3-step-error-handling article.
In this article I want to show some bad practices in error handling and maybe some quick-fixes, if you stumble
across them in your codebase.
Disclaimer, this blog-post is only a personal opinion and you will need to adjust the recommendations to your specific use-case.
I regularly stumble across code-bases where exceptions are silently ignored. Maybe the developer was in a hurry or wanted to handle the error cases later on? Anyway, this is a bad practice because if the error occurs:
/** * Reads some data from another service or repo and returns it. * The repo might throw an exception, but we don't care. */ public Mono<String> getHello() { try { return helloRepository.getHello() .map("Hello %s"::formatted); } catch (Exception e) { // do nothing return Mono.just("Hello World"); } }
In this example we might wonder why some users complain about not getting the correct hello-message, but we don't see any errors, we might think that the users are just confused.
Handle the error where it occurs, e.g. already in the Repository if possible.
If you need to handle it here, at least log the error and return a proper error-message.
Minimal refactoring
public Mono<String> getHello() { try { return helloRepository.getHello() .map("Hello %s"::formatted); } catch (Exception e) { log.error("Error reading hello data from repository - {}", e.getMessage(), e); return Mono.error(new RuntimeException("Internal error reading hello data %s".formatted(e.getMessage()), e)); } }
NOTE:
This might break upstream code since you are now returning an error instead of some default value.
Better would be to handle the error inside the reactive stream api
public Mono<String> getHelloHandling() { return helloRepository.getHello() .map("Hello %s"::formatted) // wrap the exception in a RuntimeException with a meaningful message .onErrorMap(e -> new RuntimeException("Internal error reading hello data from HelloRepository` %s". formatted(e.getMessage()), e)); }
The pattern from example one also appears in reactive streams:
public Mono<String> getHello() { return helloRepository.getHello() .map("Hello %s"::formatted) // do nothing on error, just return a default value .onErrorReturn("Hello world"); }
That looks very nice and clean, right? But we will not be able to detect that the error is thrown in the repository!
If there is a default-value, at least an error-log should be written.
As in the previous example we are wrapping the exception in another exception, this time in a custom exception that even makes it easier to detect the specific place where that exception is thrown
public Mono<String> getHello2() { return helloRepository.getHello() .map("Hello %s"::formatted) .onErrorMap( e -> new CustomException("Error reading hello-data from repository - %s".formatted(e.getMessage()), e)); }
The very opposite of silently dropping exceptions is logging the same exception multiple times. This is a bad practice because it confuses the logs with hundreds or thousands of lines of stacktraces without providing any additional meaning.
In my worst example I found the same stacktrace five times in the logs with no meaningful message at all.
The controller:
@RestController @AllArgsConstructor @Slf4j public class HelloController { private final HelloService helloService; @GetMapping("/hello") public Mono<ResponseEntity<String>> hello() { return helloService.getHello() .map(ResponseEntity::ok) .defaultIfEmpty(ResponseEntity.notFound().build()) .onErrorResume(e -> { log.error("Error:", e); return Mono.error(e); }); } }
And in the Service:
@Service @AllArgsConstructor @Slf4j public class HelloService { private final HelloRepository helloRepository; /** * Reads some data from another service or repo and returns it. */ public Mono<String> getHello() { return helloRepository.getHello() .map("Hello %s"::formatted) .onErrorResume(e -> { log.error("Error:", e); return Mono.error(e); }); } }
... and probably in some more places...
This is explained in my previous article 3-step-error-handling, therefore I will not show the code here again, but rather a recommendation:
Catching generic exceptions like Exception or Throwable can lead to unintended behaviour and make debugging quite difficult. It's better to catch specific exceptions.
public Mono<String> getHello() { try { return helloRepository.getHello(); } catch (Exception e) { log.error("Error while fetching hello data", e); return Mono.empty(); } }
Catch specific exceptions to handle different error scenarios appropriately.
public Mono<String> getHello() { try { return helloRepository.getHello(); } catch (SQLException e) { return Mono.error(new HelloDataException("Database error while getting hello-data - %s".formatted(e.getMessage()), e)); } catch (IOException e) { // maybe perform a retry? return Mono.error(new HelloDataException("IO error while getting hello-data - %s".formatted(e.getMessage()), e)); } }
and the equivalent using the reactive stream api
public Mono<String> getHello() { return helloRepository.getHello() .onErrorMap(SQLException.class, e -> new HelloDataException("Database error while getting hello-data - %s".formatted(e.getMessage()), e)) .onErrorMap(IOException.class, e -> new HelloDataException("IO error while getting hello-data - %s".formatted(e.getMessage()), e)); }
In my opinion, checked exceptions where a mistake in Java, they are not very useful and often lead to bad practices and cluttered code.
public void someMethod() throws IOException, SQLException { // some code that might throw an exception }
With checked exceptions you have to deal with them in the calling code, which makes it harder to use other patterns, e.g. functional programming or reactive streams or in spring the global error-handler.
Exception:
Checked exceptions are useful e.g. in library code, where you want to force the user to handle the exception.
Use unchecked exceptions for scenarios where the caller cannot reasonably be expected to recover.
public void someMethod() { try { // some code that might throw an exception } catch (IOException | SQLException e) { throw new RuntimeException("An error occurred - %s".formatted(e.getMessage()), e); } }
Using exceptions for control flow makes the code hard to understand and can lead to performance issues.
try { int value = Integer.parseInt("abc"); } catch (NumberFormatException e) { // handle the case where the string is not a number }
Use a regular flow control mechanism like an if-statement.
String value = "abc"; if (value.matches("\\d+")) { int number = Integer.parseInt(value); } else { // handle the case where the string is not a number }
In this article I showed some bad practices in error handling and how to mitigate them.
I hope you found this article helpful and maybe you can use some of the recommendations in your codebase and in your next refactoring.
The above is the detailed content of Error Handling Bad Practices. For more information, please follow other related articles on the PHP Chinese website!