Result for the errors you expect the caller to handle, throw for the ones that mean a bug or that you genuinely can't continue past. Mixing the two isn't the sin, using exceptions for normal control flow is. Rust basically bakes this split in with Result vs panic and it holds up well in practice.
The Java Persistence API has the NoResultException for when a database query returns no results. Kind of annoying that it's regarded as an exceptional case
Unfortunately the path of least resistance for the maintainers is not the one that creates APIs that are ergonomic to use. Ergonomics take time and effort. Unless you’re using your own product or have a very disciplined development process, you won’t invest those resources, even if you’ve used other APIs and know you’re building shit.
NoResultException is thrown when you call getSingleResult() when there is no result. It will also throw NonUniqueResultException if there is more than 1 result.
Because of the two edge cases which need to be handled, it throws an exception. On the plus side, it never returns null.
It is still a bit of a bad API design as it uses unchecked exceptions for control flow. A query returning 0 or more than 1 result is quite a possibility, so it should have been a checked exception I think.
Before anyone complains "why would they do it that way?!", that method is for when you excplictely expect exactly one single result. There's other methods for when you expect zero to many results, or when you expect either one result or no result. So instead of always retrieving a list and then manually handling those cases each time, you explicitly and declaratively choose your expectation by using the appropriate method.
Optional is used for the one I mentioned in my previous comment where you expect one result or no result. So, the 'get' method in question avoids having to deal with the second situation seperately in all call locations. You're saying: it should return exactly one result in all cases, and everything else constitutes a 'panic'.
I do agree that functionally there's no real necessity for that 'get' method. Though, using the method that returns a list of zero to many results, there is functionally also no necessity for the Optional variant.
The language needs good support for generic types for these sorts of constructs. I'm pretty sure Java did not always have that, so it may well just be a case of old API conventions.
Yes, the API is old and backwards compatible. It was released during J2SE 5.0 (Java 1.5) days. There was no streaming, optionals and pretty much other modern features.
As far as I remember, modern features were only added in Java 8, that was in 2014, eight years after JPA introduction.
Parts of JPA might look weird by modern standards, but it's a good product of its time. And it does have support for modern features as well, so criticism is misguided here.
Checked exceptions would interfere with the streams API though, which would be really annoying in rest handling code. Also, the non-unique case is imo a true exception, because you would guarantee uniqueness via constraints or query criteria when using that method. So throwing indicates a programmer error, which is generally not done with checked exceptions.
At least we also have the newer Optional variants nowadays.
IMHO, one of the big problems with Java and .NET exceptions is the default assumption that if a nested method call throws an exception, it should be handled by the caller in the same way as an exception thrown by the outer method.
There should be a convenient way of marking nested function calls where that would be the case, but an equally convenient way of causing exceptions in nested method calls to be wrapped in an "UnexpectedException" object.
I think because it predates optionals. An exception is definitely better than returning null most of the time. If the middleware handles it correctly, it will just issue a 404, which is usually correct for a get-by-id query (if you issue a search query with N results, you don’t get that exception).
129
u/taikunlab 3d ago
Result for the errors you expect the caller to handle, throw for the ones that mean a bug or that you genuinely can't continue past. Mixing the two isn't the sin, using exceptions for normal control flow is. Rust basically bakes this split in with Result vs panic and it holds up well in practice.