A bit of history:
At the beginning, there were no exceptions.
40 years ago in C:
35y ago, C++ brought invisible exceptions which propagate in the call stack and terminate the program execution.
Then, 25 years ago, Java tried to fix the problem with Checked vs Runtime exceptions, to force the developer to realize about some particular exceptions.
In web apps today, when handling exceptions, we don't recover. Let's see some Exceptions related anti-patterns
Diaper anti-pattern AKA Swallowing Exceptions
It catches everything, even runtime bugs.I
t is very dangerous, never do that.
RuntimeExceptions won the War!
because they don't annoy us, we don't see them.
The question you could ask is what should I know for instance, my app throw a IOException?
Because this is what we call an "ABSTRACTION LEAK": when you wrap Checked exceptions in Runtimes ones hiding relevant details of the disturbance.
Be aware of the Log-Rethrow anti-pattern
making the same exception being logged multiple times
It is motivated by developer's fear wondering "What if the exception slips unlogged?"
The solution to log-rethrow anti-pattern is to rely on a global exception handler
which log and reports any unhandled exceptions
To apply the global exception handler, you should only allowed RuntimeExceptions in the core logic.
So converts the appearing checked exceptions (ie from librairies) into runtime exceptions.
Now you handled the errors, you have to present them to the user.
What should be shown to the users? (Typically in the return of get request).
First point, do never show a stack trace with security details
where it apperas version of JDK, lib --from which security flaws are known--
Handle that in the Global exception handler:
Display the exception's message is OK for small apps.
You can enhance the error output with a custom RuntimeException containing an enum referencing error codes, and their related labels loaded from properties.
Then test it in different languages (regarding properties) by changing the browser locale (put the selected language on top);
So to recap:
1)We have a custom exception containing an enum:
2) A properties file:.
message.properties _fr _ro _es
Now we know how to present the errors to the user, we might need to debug our app, to fix the user disturbance.
Here we go with the "Catch Rethrow With Debug Pattern".
This will allow you to find the value of some key variable ('id' in above code).
Never forget to parse it with the source exception. Never decapitate your exceptions!
Okay, so now we understand the "Catch Rethrow With Debug Pattern", let's list the reasons of its use:
- 1) It conveys "user-friendly" message
- (with the enum code...)
- 2) It eases testing
- (with the enum code...)
- 3) It helps developers
(by rethrowing with debug message)
- 4) It is natively supported in Lombok @SneakyThrows
(typically for use-case fatalerrors)
Besides, what is the most famous exceptions? It's the NullPointerExceptions AKA NPE. Let's talk about it!
We don't defend against NULL, we charge against it! By throwing it early (for constraints)
There are several tactics, in constructor :
- 1) check if null and throw an IllegalArgumentException
- 2) use assert param !=null;
- (Beware that assertions can be disable at runtime)
- 3) use Objects.requireNonNull(param);
- 4) use Lombok @NonNull annotation
(in the constructor signature before param declaration)
- 5) return an Optional in the getter of your nullable field and in the call use myOptional.map(opt->opt.getMyField()).orElse(0);
Nice! We browse how to behave toward NPE.
Let's look at a nice addition of jool from jooq: Unchecked.function(...)
Java 8 brought the functional interfaces, but functional interfaces do not throw exception.
That's here Unchecked.function(myfunction) comes into play.
Unchecked.function embedd your Lambda and handle exceptions launched from the lambda.
That way you can inline exception prone lambda like this:
You got it! Nice... Let's see another way with Vavr (io.vavr):
Using Vavr Try class:
So, this parse and collect all dates that are valid. This is possible because Try<> can hold either a result or an exception by collecting both results and exceptions in a single pass.
Great, what a pleasant flow of intel! But let's review the key points:
- 1) Use Runtime; @SneakyThrows; Unchecked for lamdas (jool)
- 2) Anti-patterns: Diaper, Decapitation, Log-Rethrow
- 3) Global Exception Handler
- 4) Enum error codes for users or tests
- 5) Catch-Rethrow-with-Debug
- 6) Defeating NPE with early-throw, or Optional
- 7) Try (vavr)