However, for application languages that can afford a few extra nicities like garbage collection, I fail to understand why the stackless coroutine model (`suspend` in Kotlin) or `async`/`await` continue to be the developer's choice. Why do languages like Kotlin adopt these features, specifically?
Manually deciding where to yield in order to avoid blocking a kernel thread seems outside of the domain of problems that those using a _higher level_ language want to solve, surely?
The caller should decide whether to do something "in the background". And this applies to non-IO capabilities too, as sometimes pure computations are also expensive enough to warrant not blocking the current task.
Go and Erlang seem to have nailed this, so I'm glad Java is following in their footsteps rather than the more questionnable strategy of C# and Kotlin. (Lua's coroutines and Scheme's `call-with-current-continuation` deserve an honourable mention too.)
Kotlin will still support Loom on JVM and there will likely be integration with suspend/flows etc also.
The problem with Kotlin and the like is that they can't easily compile away their features due to inherent runtime dependencies, e.g. garbage collection, making them poorly suited to environments with a very minimal runtime like WASM, while also being at the mercy of the host language creating runtime abstractions that have mismatches with their own language's features.
Although it'd be unfair for me to say the JVM is designed only for Java; invokedynamic and non-reified generics both assist JVM targetting for non-Java languages such as Clojure.