Got async/await working in CLJS with a couple of small changes. Working test:
(ns cljs.async-await-test
(:require [clojure.test :refer [deftest is async]]))
(defn ^:async foo [n]
(let [x (js-await (js/Promise.resolve 10))
y (let [y (js-await (js/Promise.resolve 20))]
(inc y))
;; not async
f (fn [] 20)]
(+ n x y (f))))
(deftest async-await-test
(async done
(-> (foo 10)
(.then
(fn [v]
(is (= 61 v))))
(.finally done))))
Branch: https://github.com/borkdude/clojurescript/tree/js-await
I'm sure there's a few edge cases, but can iron those out later.What came out of the discussion:
• ClojureDart and squint are the only dialects that use async/await that I'm aware of
• async is a better name than js-await if we want to be cross-dialect compatible
• ClojureDart and squint both use the ^:async keyword on functions. ClojureDart supports it on the form for anon. fns, Squint supports it both on the form and on the fn or fn* symbol.
• For CLJS it's better to direct people to use ^:async on the fn symbol since putting it on the form generates a MetaFn which isn't great for interop with JS
• I'll ask ClojureDart to also support :async on the fn symbol
does ClojureDart not also have the MetaFn problem?
I don't know ClojureDart well enough to say but I forwarded the question. Meanwhile I'm done with the changes in ClojureScript to support async/await now! https://github.com/clojure/clojurescript/compare/master...borkdude:clojurescript:js-await?expand=1 I can write up a JIRA issue + patch later this week, but if you already have some feedback prior to that, let me know.
Pretty nice that I was able to use it inside the tests for it. E.g.:
(deftest await-in-loop-test
(async done
(try
(let [f (^:async fn [] (loop [xs (map #(js/Promise.resolve %) [1 2 3])
ys []]
(if (seq xs)
(let [x (first xs)
v (await x)]
(recur (rest xs) (conj ys v)))
ys)))
v (await (f))]
(is (= [1 2 3] v)))
(catch :default e (prn :should-not-reach-here e))
(finally (done)))))That's a good consideration, I'll consult with other dialects about this
ClojureDart has async await too I think
yeah, I think if we can all align that would be a win
Started the dialect conversation here in the #clojuredart channel. https://clojurians.slack.com/archives/C03A6GE8D32/p1767456319311549
@borkdude Just asking out of curiosity (I don't know this topic deeply) - will it be possible to use await in tests, thereby making it possible to write IO/API tests/assertions in a sequential order? (And how does this impact cljs.test/async?)
@kumarshantanu Yes. Already possible.
(deftest foo
(async done
(try (await blablabla)
(finally (done)))))According to this survey, async/await is the most asked feature in CLJS. http://state-of-clojurescript.com/
yeah
E.g. TODO: • anonymous functions should also be able to work async.
@dnolen Any interest to add this once it's done?
@borkdude This is very interesting. However, I am curious about the nomenclature (though it's a draft) - why not ^:js/async and js/await to be consistent with other JS-specific API? The difference gets heightened in CLJC files.
js/async would not be correct, since then it would refer to a global await function. await is a reserved keyword in JS. That is exactly why I prefix it with js- like other reserved keywords in CLJS, e.g. js-in, js-delete etc.
^:js/async vs ^:async: the js/ prefix feels unnecessary to me here. The above is exactly the syntax I've been using in squint for 3+ years now.
@borkdude interesting! The definition of foo makes a lot of sense to me, but I don't really get what async-await-test is doing. What would the javascript equivalent of that (`async-await-test` ) be?
oh, I hadn't known about this: https://clojurescript.org/tools/testing#async-testing 🆒
The pure promise version of foo has obvious readability problems:
(defn foo&
[n]
(let [f (fn [] 20)]
(-> (js/Promise.resolve 10)
(.then (fn [x]
(-> (js/Promise.resolve 20)
(.then (fn [y]
(let [y (inc y)]
(+ n x y (f)))))))))))
Which is why we currently prefer the [shadow.cljs.modern :refer (js-await)] version:
(defn foo-theller-await&
[n]
(js-await [x (js/Promise.resolve 10)]
(js-await [y (js/Promise.resolve 20)]
(let [y (inc y)
f (fn [] 20)]
(+ n x y (f))))))
But the proposed defn ^:async foo version is the best, as it gives complete control over when to await trees/chains of async function calls (or not!), while avoiding indentation in the same way as await in js does. 👍
I'm not sure about the implementation, but the interface strikes me as correct, this morning.Progress: https://mastodon.social/@borkdude/115827239557045244
My 2c: .NET also has async-await. So, when/if ClojureCLR adopts that support I'd expect the syntax to be consistent with what CLJS adopts, unless the keywords are qualified appropriately.
To answer your question on ClojureDart: they don't support meta fns, metadata on functions is just dropped
Patch for async/await. https://clojure.atlassian.net/browse/CLJS-3470
Ok, will find some time to take a look
Thanks for working on this!!!
Someone on Twitter suggested that the go IOC macro could possibly be rewritten to emit async/await too so we can have much smaller code for core.async. I haven't thought about that, but perhaps something to pursue after this. But first I think adding support for generator fns would be nice too. It's just a small similar change on top of this patch. If you want me to do that next, just poke me.
(It's 2026 and jira still renders .patch files as a .doc )
We also need some work on core.async, right?
probably <! is not supported across await ?!
I think it's interesting, at least, to have a good error message.
re: core.async yes that might be possible, definitely crossed minds before
hm ok, one downside with async/await is using binding and dynamic vars which will not work the way it looks it will.
It's a known problem that binding won't work well with promises, but the await syntax may mislead people into thinking it will
(deftest dynamic-binding-test
(async done
(try
(let [f (^:async fn []
(with-out-str
(print (await (js/Promise. (fn [resolve]
(js/setTimeout #(do
(print "in promise;")
(resolve "resolved")) 1000)))))))
promises (repeatedly 1000 f)
strs (await (js/Promise.all promises))]
(is (= (repeat 1000 "in promise;resolved") (vec strs))))
(catch :default e (prn :should-not-reach-here e))
(finally (done)))))
Here you have a fn f that contains an await .When calling await, the thread can be parked so other work can be done. Here I run 1000 calls in parallel.
After running the test, prn is about to writing to a stringbuffer still since one or more of the running f have captured the old value of prn which is actually the new value that another running f is working with.
cljs.user=> (str *print-fn*)
"function (x__12346__auto__){\nreturn sb__12345__auto__.append(x__12346__auto__);\n}"
So it's not "binding" safe. Again, this isn't a bug, since it's the same when you would have written the code above using Promise and .then but it might be counter-intuitive.Maybe [using](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/using) can be used to implement bindings? Setting a global value and cleaning up afterwards.
@dnolen We can however solve this by conveying bindings to the await body and restoring them afterwards.
(await body) =>
((fn [] (old_val = binding;
x = await (~body)
binding = old_val;
return x;)))I don't think CLJS currently keeps track of bound dyn vars though.
One alternative is to just warn folks about not using binding across await
Can implement a clj-kondo rule for this.
It seems go blocks also don't convey bindings in CLJS. > The Clojure core.async captures bindings at the beginning of the go block. This could be done in ClojureScript core.async as well but it needs language support to do this correctly and efficiently. > > David So maybe it's not as huge of an issue if it's been like that in CLJS for go blocks
@wcohen I’ll address your findings in JIRA, thanks 🙏
sorry for delayed response, binding conveyance is just not straightforward - not a blocker for this work
@borkdude the only reason I have even found this is I’m going down a bit of a rabbit hole re core.async here — it may not work out but this async/await plus potentially a node/browser agnostic approach to web workers (abstracted away for the user) might actually be able to create a defmacro thread and/or <!! >!! for core.async on cljs, where instead of ExecutorService we have a pool of workers. I’m not sure if I’ll be able to work out the snags all the way on my own, but I’m giving it a shot as a first proof-of-concept
New patch uploaded with additional changes to defn to support multi-arity + variadic combinations