cljs-dev

borkdude 2026-01-01T16:36:43.046619Z

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.

borkdude 2026-01-04T20:05:31.772049Z

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

dnolen 2026-01-04T20:15:19.245169Z

does ClojureDart not also have the MetaFn problem?

borkdude 2026-01-04T20:24:02.372919Z

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.

borkdude 2026-01-04T20:26:29.306139Z

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)))))

🆒 1
borkdude 2026-01-03T08:50:41.108869Z

That's a good consideration, I'll consult with other dialects about this

borkdude 2026-01-03T08:50:53.915289Z

ClojureDart has async await too I think

dnolen 2026-01-03T14:32:44.945969Z

yeah, I think if we can all align that would be a win

borkdude 2026-01-03T16:50:12.098079Z

Started the dialect conversation here in the #clojuredart channel. https://clojurians.slack.com/archives/C03A6GE8D32/p1767456319311549

Shantanu Kumar 2026-01-16T08:28:24.922159Z

@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?)

borkdude 2026-01-16T08:45:53.601919Z

@kumarshantanu Yes. Already possible.

(deftest foo
  (async done
     (try (await blablabla)
       (finally (done)))))

👏🏽 1
borkdude 2026-01-14T12:59:28.106959Z

According to this survey, async/await is the most asked feature in CLJS. http://state-of-clojurescript.com/

👀 1
dnolen 2026-01-14T15:22:58.946189Z

yeah

borkdude 2026-01-01T16:38:15.419129Z

E.g. TODO: • anonymous functions should also be able to work async.

borkdude 2026-01-01T16:38:31.120819Z

@dnolen Any interest to add this once it's done?

Shantanu Kumar 2026-01-01T17:02:39.155549Z

@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.

borkdude 2026-01-01T17:06:24.111099Z

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.

Harold 2026-01-01T17:09:43.240359Z

@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?

Harold 2026-01-01T17:12:50.461499Z

oh, I hadn't known about this: https://clojurescript.org/tools/testing#async-testing 🆒

Harold 2026-01-01T17:25:05.103369Z

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.

borkdude 2026-01-02T19:51:22.072299Z

Progress: https://mastodon.social/@borkdude/115827239557045244

👍🏽 1
Shantanu Kumar 2026-01-03T05:31:31.123859Z

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.

borkdude 2026-01-05T09:07:34.520689Z

To answer your question on ClojureDart: they don't support meta fns, metadata on functions is just dropped

borkdude 2026-01-05T11:07:42.706249Z

Patch for async/await. https://clojure.atlassian.net/browse/CLJS-3470

dnolen 2026-01-05T11:32:45.392809Z

Ok, will find some time to take a look

dnolen 2026-01-05T11:33:08.624589Z

Thanks for working on this!!!

👍 2
borkdude 2026-01-05T11:34:37.216149Z

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.

souenzzo 2026-01-05T11:54:09.063519Z

(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.

dnolen 2026-01-05T12:12:19.129159Z

re: core.async yes that might be possible, definitely crossed minds before

borkdude 2026-01-05T13:43:28.017259Z

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.

souenzzo 2026-01-05T13:51:50.985509Z

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.

borkdude 2026-01-05T13:52:08.149039Z

@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;)))

borkdude 2026-01-05T13:55:46.881359Z

I don't think CLJS currently keeps track of bound dyn vars though.

borkdude 2026-01-05T13:57:06.757309Z

One alternative is to just warn folks about not using binding across await

borkdude 2026-01-05T13:59:22.496419Z

Can implement a clj-kondo rule for this.

borkdude 2026-01-05T14:17:41.131919Z

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

borkdude 2026-01-07T15:15:08.537239Z

@wcohen I’ll address your findings in JIRA, thanks 🙏

dnolen 2026-01-07T15:34:37.776299Z

sorry for delayed response, binding conveyance is just not straightforward - not a blocker for this work

👍 1
wcohen 2026-01-07T15:58:58.749469Z

@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

borkdude 2026-01-07T17:15:09.809509Z

New patch uploaded with additional changes to defn to support multi-arity + variadic combinations