Different handling of checked vs unchecked exceptions in DSLContext.transaction is unexpected when using jOOQ from Kotlin

191 views
Skip to first unread message

Mark Amery

unread,
Jul 2, 2023, 11:29:30 AM7/2/23
to jOOQ User Group
I am a dev with close to zero experience writing Java who currently works on a Kotlin web application that uses jOOQ - my first significant application in a JVM language. As such, I frequently find myself surprised when I encounter interfaces or behaviours in Java libraries that seen to make little sense from a Kotlin perspective. This message is about such an experience with jOOQ.

The Kotlin web application has a global exception handler; it has some special exception types that trigger special behavior in the global error handler; and at various places the application does stuff in transactions using code like this:

    dsl.transaction() { config ->
        // Logic that might throw exceptions
    }

As it happens, some of the types of exception we have special handling for in the global error handler extend from `Exception`, while others extend from `RuntimeException` - a fact I was not especially conscious of and that is mostly of no significance in Kotlin, where unchecked exceptions don't exist and `throw Exception("some exception message")` is a perfectly acceptable idiom. Imagine my surprise, then, to discover that SOME of those special exceptions, if I throw them from within a `DSLContext.transaction()` callback, don't trigger their special behaviours in the global exception handler, which instead receives a DataAccessException. Some digging revealed that this code in `transactionResult0` was to blame, which handles exceptions of types that inherit from `RuntimeException` differently from those whose type inherits directly from `Exception` - specifically by wrapping the latter in a `DataAccessException` instead of rethrowing them directly:

    // [#6608] [#7167] Errors are no longer handled differently
    if (cause instanceof RuntimeException e)
        throw e;
    else if (cause instanceof Error e)
        throw e;
    else
        throw new DataAccessException(committed
            ? "Exception after commit"
            : "Rollback caused"
            , cause
        );

No doubt this behavior seems intuitive and reasonable to Java programmers, who work in a world with checked exceptions. To a dev using jOOQ from Kotlin and unfamiliar with Java, though, it felt rather like an arbitrary trap that didn't need to exist, and my first instinct was to file a bug report. After all, jOOQ officially supports Kotlin, so shouldn't it refrain from doing this arbitrary wrapping of (some) exceptions that has no reason to happen from a Kotlin perspective?

On reflection, though, I'm struggling to figure out how I ought to feel about this. What's the "correct" behavior for jOOQ to have here? Should anything change?

I thought for a while about whether jOOQ should behave differently when being used "from Kotlin" than when being used "from Java", but I think that - given that Java code can call functions defined Kotlin code that calls functions defined in Java code and so on ad infinitum - the very concept of jOOQ being used "from Kotlin" or "from Java" is probably too ill-defined for this to make sense. Doing something like having behavior change based on the inclusion of the jOOQ-kotlin package also seems like a disgusting trap in its own right.

That leaves me with a couple of thoughts on things that jOOQ could do that might be good ideas:

1. Maybe that block I quoted above should just unconditionally throw `e`? Obviously just `throw e` won't even compile, but there are hacks (see e.g. https://stackoverflow.com/q/31316581/1709587) to let you throw exceptions of checked types without listing them in a `throws` clause in the method declaration, so it is at least POSSIBLE for jOOQ to behave this way. Ignoring backwards compatibility, would this indeed be the better behavior for jOOQ to have? If so, is it worth breaking backwards compatibility over?

2. Maybe having some Kotlin-specific alternative to `DSLContext.transaction` (and whatever other methods are affected by this same issue) in jOOQ-kotlin? This could probably be a simple wrapper around `DSLContext.transaction` that "unwraps" exceptions that are wrapped in `DataAccessException`.

Alternatively, perhaps the right perspective on this is that we Kotlin programmers are newcomers in Java lands, and need to assimilate into the local culture - including understanding Java practices like handling Exceptions and RuntimeExceptions differently, and anticipating their use. From this perspective, perhaps jOOQ is doing nothing wrong or ill-advised whatsoever, and I've simply learned a necessary lesson about Java. Certainly a workaround is possible: I can implement the wrapper around `DSLContext.transaction` that I contemplated above in my application and use it instead of using `DSLContext.transaction` directly.

What's the right way to think about this, do you reckon? Should anything in jOOQ change?

Cheers,
Mark

Lukas Eder

unread,
Jul 3, 2023, 3:15:33 AM7/3/23
to jooq...@googlegroups.com
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





--
You received this message because you are subscribed to the Google Groups "jOOQ User Group" group.
To unsubscribe from this group and stop receiving emails from it, send an email to jooq-user+...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/jooq-user/c7a318bb-4f76-4734-ba2a-eb5afba20533n%40googlegroups.com.

Mark Amery

unread,
Jul 3, 2023, 9:53:30 AM7/3/23
to jOOQ User Group
Very interesting, and thanks for the reply.

I hadn't realised that type parameters could be used in the "throws" clause of a generic method in Java! That's pretty cool! I am in no position to speak authoritatively since I am not a Java dev, but the more I think about it the more I think your "throws T" approach would actually be the perfect approach here if it weren't for backwards-compatibility concerns, not just for the sake of Kotlin devs but also for Java devs. After all, if the user passes a lambda to `transaction` that throws a checked exception, which they'd be forced to explicitly catch and handle without jOOQ in the middle, isn't it a bit weird for jOOQ to intercept that and neutralise the enforcement the compiler would otherwise provide that the dev has remembered to handle the exception? jOOQ does this right now even for what you call "business exceptions"; if I throw such a checked business exception from within the lambda passed to `transaction`, jOOQ catches it and wraps it in a DataAccessException - effectively telling me "nope, you don't need to worry about catching that checked business exception". That seems like the wrong behaviour on jOOQ's part!

Some tinkering around with toy Java programs in IntelliJ suggests to me that the Java compiler IS clever enough to infer what checked exceptions can be thrown from a lambda and that the `throws T` approach would therefore behave the way we'd want it to - i.e. propagating the compile-time need for checked exceptions to be declared or caught up to the caller of transaction(). (Probably you already know this, but I wasn't sure until I tinkered and so I note it here for anyone else in doubt.) This seems really nice to me; IMO the behaviour you'd get with `throws T` is the perfect behaviour to have here in both Java and Kotlin and would be a no-brainer if it were not a backwards-compatibility break. What a shame that it is!

Anyway, I might go ahead and create an issue as you suggest and see if I stir up further discussion.

Cheers,
Mark

Lukas Eder

unread,
Jul 3, 2023, 10:28:28 AM7/3/23
to jooq...@googlegroups.com
You're maybe overlooking the fact that jOOQ is consistent with this behaviour throughout its API. For example, DSLContext::connection or DSLContext::batched work the same way.

Any change would be a *significant* change and is thus unlikely, even if you now think that your design would be more desirable than the status quo, of which I'm not entirely convinced.

Reply all
Reply to author
Forward
0 new messages