Hi Mark,
Thanks for your message.
It's even worse than you describe. The Scala folks went ahead and declared "Throwable is all you ever need," i.e. to them the distinction between Error and Exception is useless. Which it is, if you assume that Throwables are all just Java things that Scala developers tend to avoid entirely because of a perfect monadic world (they tend wrap all Java code in some IO monad to make stuff more idiomatic).
Even within the Java community, not everyone agrees that this wrapping of exceptions in chains of unchecked exceptions is really valuable. The "idiom" was introduced by JavaEE and Spring, I believe? Every API adds their own exception class hierarchy, wrapping the underlying API's hierarchies. For example, using Spring+jOOQ+JDBC, you'd get Spring exceptions wrapping jOOQ exceptions wrapping JDBC exceptions, all for exceptions that you usually can't really recover from, so the only useful exception in logs is the original one anyway. One reason why this might have happened is again because of checked exceptions. Back in the early 2000s, when a lot more exceptions used to be checked, you *had to* wrap, because you couldn't just throw any undeclared exception. But we learned (the hard way), that hardly anyone really benefits from this feature, while most suffer from it.
The main purpose of jOOQ's DataAccessException has always been to "hide" the checked SQLException, which is one of the two annoying JDK checked exceptions that everyone hates most (the other being IOException). Checked exceptions can be a cool feature for "business exceptions", where you want to emulate a union type in a funny way that strictly binds it to control flow (weird, but kinda cool). Other languages formalised this idiom better, e.g. using Either or other means of type checking different outcomes. In my opinion, actual union types are the best solution here. Regrettably, few languages have them (Scala 3 does, for example, or TypeScript). But neither SQLException nor IOExceptions are such business exceptions (with a few "exceptions," such as e.g. SQLIntegrityConstraintViolationException, which can indeed act like a "business exception").
Re-throwing the checked exception is certainly possible with this well-known generic erasure hack, but do note that it is not possible to catch it in Java, without annoying glue code. For example, javac will reject this program:
jshell> try {} catch (java.sql.SQLException e) {}
| Error:
| exception java.sql.SQLException is never thrown in body of corresponding try statement
| try {} catch (java.sql.SQLException e) {}
| ^--------------------------------^
So, if jOOQ re-throws the checked exception, then, currently, it wouldn't be possible to catch it. We would have to declare TransactionalRunnable et al. to be generic with the exception type, i.e.
public interface TransactionalRunnable<T extends Throwable> {
void run(Configuration c) throws T;
}
And then declare T as well in the transaction() methods. That would at least make the change compile-time incompatible, instead of just behaviourally incompatible, which is certainly better. But again, this would go against anything else a jOOQ user would expect, where the SQLException never surfaces client code. In fact, it still probably wouldn't because the exception that rolls back the transaction might be a jOOQ DataAccessException that again wraps the SQLException, so the idea here is also to avoid re-wrapping the jOOQ exception. I guess that was the main reason for the current design.
Perhaps that's the problem here? The fact that *some* exceptions don't get re-wrapped like all the others? Perhaps rather than re-throwing *everything* (and wrapping nothing), we should re-throw *nothing* (and wrap everything)?
Clearly, either change is a drastic, behaviourally incompatible change, and that alone makes it unlikely to be implemented soon.
Automatic Kotlin specific behaviour is also often not a good idea because it's very hard to document, maintain, and make sure it really is what people want. What if someone uses *both* Java and Kotlin in their code base? Wouldn't it be terrible for one Spring service to behave this way, and the other to behave that way?
The only way I can see to make this customisable would be to allow for TransactionListener instances to override the cause that will eventually be thrown. That way, users are in full control over this detail. I'm actually surprised this isn't being done, since ExecuteListeners can do this (the overriding, they also implement the same semantics regarding RuntimeException). I'll look into this detail:
But again, I can't promise that such an ability to override an exception will allow for throwing checked exceptions due to the above caveat and this being very debatable in the Java ecosystem. You could try your luck and copy paste your email to the issue tracker in a new feature request:
That way, we can collect user feedback from the kotlin community. If there's significant buzz, it at least becomes clear that the problem is important enough to address *somehow*.
I hope this helps,
Lukas