Being a hosted language, Clojure heavily relies on the underlying language and its runtime. While the JVM Clojure is praised for being so integrated with JVM that it’s easier to use Java code from Clojure than from Java itself, the same doesn’t exactly hold true for ClojureScript.
In the modern Javascript world, everything is async. Many APIs are awaitable even if there’s no explicit need for them to be, because chaining promises is easy. Unfortunately, ClojureScript doesn’t provide any first-class support for those.
The interop guide suggests several options, but the straightforward way to use them is basically the sugar-coated JS syntax of chaining .then()
s. The same document discusses core.async
, the Clojure’s take on the Go multitasking model that uses channels to pass data between concurrently running tasks.
core.async
isn’t without issues, though. It’s notorious for losing type hints, so externs inference basically doesn’t work. In practice, that means problems for any promises that return JS objects, because you want to type-hint those for closure compiler.
This loops us back to the ecosystem problem. In JVM world, there’s a substantial amount of Clojure libraries, and even more Java ones. In Node world, the amount of ClojureScript libraries is severely limited and you will end up using npm a lot. That means, you will have to interact with JS world a lot.
Now, there’s a neat alternative to core.async
, promesa offers a straightforward API for promises in both Clojure and ClojureScript. Indeed, you just change your (let [a (somefunc)] ...)
for (p/let [a (somefunc)] ...)
and somefunc
will be automatically awaited if it returns a promise. Nesting problem is solved! You also have a handful of extras, like p/do
or p/->
, all of which behave exactly as the core Clojure counterparts, but now with async awareness.
It works great, mostly. Where it breaks apart is when you need to pass the promise into the JS world, e.g. when you pass your function into something like prom-client
that can take both regular functions and async ones. Promesa’s problem is that it reimplements promises from scratch, thus the JS world cannot identify them as being awaitable.
It gets even worse when you reach into the world of tracing. OpenTelemetry, the most prominent implementation of tracing in Node, uses an obscure module, node:async_hooks
, to pass the context indirectly. Node has its own implementation of async-local storage, which is not unsimilar to thread-local storage in other languages, but in this case it’s being carried over along with the async call chain. The problem? Promesa’s promises aren’t “real”, and thus they can’t avail of this API, breaking all the traces as spans fail to chain to one another.
Luckily, I stumbled on kitchen-async. Don’t let the last commit made 5 years ago dissuade you, it’s a great library! It has the API that’s very similar to promesa, but it’s ClojureScript only, and thus it’s built on the JS’s native Promise. It has all the same p/let
s, and p/do
s, allowing you to write idiomatic promise-aware Clojure code, and uses the native runtime that just works with any other code you’ll pull from npm. No more issues of having runtime incompatibilities!
It’s not the first time the issues with ClojureScript make people wonder what’d it take to have ClojureScript 2.0, and indeed, projects like cherry and squint exist. With squint, your async/await code in Clojure would look very much the same as the JS code and compile to native async/await, too. Ideally, I’d wish to see ClojureScript making a shift towards the modern Javascript to provide a better interop story. I don’t think I could write a pure ClojureScript application with zero npm dependencies any time soon.