does anyone know if ClojureScript will eventually get a way to generate JS async and await? I've looked into this years ago and ended up using macros and Promises like promesa. But if I do this, stack traces don't work on node.js + serverless. They do work in the browser. This makes debugging async code very challenging in CLJS.
FWIW squint and cherry have it right now. Don't know if CLJS will.
there is https://clojureverse.org/t/promise-handling-in-cljs-using-js-await/8998 as well if using shadow-cljs
Is that actually generating async await?
I thought that was the OP question
no, it doesn't. but it does use native promises, so doesn't have the stacktrace issue (at least the last time I checked). it is also very close to the metal, so you won't see anything of the "library" on the stacktrace either.
Ah native promises. Eventually other libs like promesa do use native promises too right? Agree that it does more than it should in CLjS. Contemplating a promesa-lite version ;-)
no clue, never used promesa myself, but there is a lot of "wrapper" code going on.
js-template basically only acts like str, it doesn't do any hiccup conversion and imho shouldn't. there could be another macro that does that conversion though, given that its a common use case.
I don't mean: does it do hiccup, but: is it compose-able with something that uses hiccup (I don't think it is but it would be great if it was possible somehow)
Another thought: now that ES6 is the baseline for CLjS we could simply let str emit a template literal and a tag on str changes the template function
In squint str already does so (except for the tag)
no clue what you mean by composable with something that uses hiccup.
did you do some benchmarks to test whether js templates are actually faster than the concatenation done by str?
They are equally fast. And since they are equally fast there's no reason we could just use plain str + tag for template literals
I'll postpone the hiccup discussion to not confuse the discussion but the main issue is that writing html in strings for lit gets very tedious compared to hiccup
dunno how you'd add tags to str given that (str not-at-tag "foo") is probably common (`js-template` has the same problem, but by definition uses the first symbol as tag)
As a tag on the str form
you mean override what ^lit/html (str "foo" bar) does? unsure what you mean by tag? many different ways of tagging in CLJS ๐
Syntax wise yes, but what do you mean with override? Currently the tag here is a no-op?
not a no-op, it is added as the :ret-tag (or whatever that was exactly). rarely used, but used.
True. If we expand str in a js* expr with string tag, would it fix that?
sorry, I need to see code example to follow. unsure how js* factors into this
Look at the current expansion of str, it will become clear
(Not at kbd right now)
What I meant is this: https://github.com/clojure/clojurescript/blob/c4295f303100bbf5afac449242d30bca1126f1a1/src/main/clojure/cljs/core.cljc#L891
I have this working, in case you're interested.
cljs.user=> (defn my-string-template [xs & ys] (prn xs ys) "hello")
#'cljs.user/my-string-template
cljs.user=> (let [x 1] ^my-string-template (str2 1 2 3 x))
#js ["123" ""] (1)
"hello"and string type hint still works because of macroexpansion with the right tag:
cljs.user=> (let [x 1 y ^my-string-template (str2 1 2 3 x)] (inc y))
WARNING: cljs.core/+, all arguments must be numbers, got [string number] instead at line 1
#js ["123" ""] (1)
"hello1" No idea why number is included in the warning, but this is the same warning as with:
cljs.user=> (let [x "foo"] (inc x))
WARNING: cljs.core/+, all arguments must be numbers, got [string number] instead at line 1 <cljs repl>
"foo1"code here: https://github.com/borkdude/clojurescript/commit/ac3935a6fb92e217e7d2dd81e2a4164bf6034bb4
switching over str2 to str: almost all tests passing (some compiler tests are failing, but will look into it once we agree that this is a promising direction)
I havenโt been paying too much attention to squint and shadowโs implementations, but I wrote this proposal in 2018 that might still be relevant.
https://observablehq.com/@shaunlebron/proposal-generators-and-async-functions-in-clojurescript
@shaunlebron squint pretty much does what your proposal suggests but without the js- prefix in the keywords of the metadata and separate keywords for async + gen. It does use js-await and js-yield syntax. Try it on the squint playground
perfect, yeah I like that better
async was the top missing language feature in the "state of cljs" 2024 survey.
all those features have been available in shadow-cljs for 4+ years ๐ https://clojureverse.org/t/modern-js-with-cljs-class-and-template-literals/7450
well minus static typing of course
in squint/cherry too :) (minus static typing)
but shadow-cljs js-async is nothing special compared to others libraries? It does not provide the syntax one would except. I use (a fork of) kitchen-async that allows to use it in many clojure forms + uses native Promise objects. I moved out from promesa but can't remember why. squint/cherry in comparison is the true async/await feature we are expecting (I guess)
I actually prefer the js-await syntax, since its immediately obvious where the async block action starts just by looking at the first word. digging for some nested await somewhere is less clear for me.
(js-await [x (some-code)]
(occurs-when-async-action-completes x))
(let [x (await (some-code))]
(occurs-when-async-action-completes x))but I wrote it, so no surprise there ๐
in squint it's js-await btw to align with the other js-reserved keywords, js-in, etc.
it just break the flow so that's why I don't like it. You can't use it inside a let, threading macros, etc. That's why (for me) promesa/kitchen-async is already better than js-await, but still not ideal as you don't control what is awaited or not (everything is wrapped by default)
I exactly do not want it to work in arbitrary positions since it just makes it very hard to know where an async split occurs, but yeah I get that it is different.
That's the point of the native JS async/await, you don't want to think much about where it splits. Else you can still use the then-fn/body syntax as your js-await (or any equivalent). But then I would not call shadow-cljs js-await the async/await feature. It's more a "js-then" (imho)
(just to be clear, disagreeing on this doesnโt mean anything, I love shadow-cljs, use it all the time, and Iโm grateful to you for it. Amazing work)
we all love shadow-cljs, no argument there :)
I got no issue with disagreements, I think thats useful. ๐ I think mostly the difference in design is due to either implementing it as a language feature with compiler support, or just a macro that can work anywhere with no adjustments to the compiler whatsoever. js-await is just that, a true async/await is not. I'm not saying that there shouldn't be compiler support, just that js-await is a trivial hack that has covered 100% of my uses to this date ๐
I agree except I wouldn't call js-await a hack, it's just a macro. I also use the (-> thenable (.then (fn [x] ...)) syntax sometimes.
We also use [shadow.cljs.modern :refer (js-await)] and prefer it to both promisea and writing -> ... .then
I have the same question as OP though, there are cases where I'd like to write cljs code that compiles to real js async functions and await calls to them.
An important one that will impact us eventually is AWS node lambda discontinuing support for callback-based handlers in their Node 24 runtime:
โข https://dev.classmethod.jp/en/articles/aws-lambda-node-js-24/#3.%2520Verifying%2520Behavior%2520of%2520Deprecated%2520Callback%2520Format
This is going to necessitate us eventually rewriting our node lambda handlers (we make use of the callback-style) and doing so with async/await in a way that avoids code nesting the way js programmers get to would be nice.
why would you need to use async/await generated code instead of just dealing with raw promises via macros? AWS shouldn't care about that since you can do that in JS too
anyway, if you really need to, squint or cherry could be a good tool to get async/await today
async functions are just regular functions that always return a promise. so if you make sure that your regular function always returns a promise then you have an async function ๐
some JS tools check for the AsyncFunction contructor I believe
> x = async function() {}
[AsyncFunction: x]
> x.constructor
[Function: AsyncFunction]
>
(for reasons unbeknownst to me but I know some do)really? never seen that, but no doubt it exists ๐
> async functions are just regular functions that always return a promise. so if you make sure that your regular function always returns a promise then you have an async function
This is only half of it. async functions also use generators under the hood to respect await calls inside them. Writing code in this way can really simplify certain things.
> anyway, if you really need to, squint or cherry could be a good tool to get async/await today I agree with this for some programs - however, the programs I am working on need real cljs
> why would you need to use async/await generated code instead of just dealing with raw promises via macros? AWS shouldn't care about that since you can do that in JS too
also agreed that it's possible to express w/ raw promises, the code is just simpler if it can be written straight-line with async/await
> need real cljs can you explain what you mean with real?
I mean, I understand squint and cherry aren't the real CLJS, just curious what feature you mean specifically
We need npm deps, and GCL + GCC w/ advanced optimizations.
Honestly, I tried the first version of these systems in squint a couple years ago, it would have been neat if we could have gone that route, but that ship has for now sailed (:
These programs are unusually sensitive to js artifact sizes, and squint can win that every time compared with cljs
"These programs are unusually sensitive to js artifact sizes," can you explain more?
The smaller the zipped js is, the faster my program starts, and the more money I make ๐ฎ
make sense :)
we are in fact pondering async/await and generators - I do like these features of squint. template strings has come up and I think that could be an easy one, #js "foo" seems fine to me for this
ideally we could get this stuff into a release in 2026 sooner rather than later. Any help to get there appreciated of course.
We can stay on Node 22 into 2027 (https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html), so this is not urgent for us I think generators + async/await in cljs would be super-fun. I don't know if I'm smart enough to come up with the best possible cljs constructs - I do know that it shouldn't involve indentation ๐
#js "foo ${(* 6 7)}" โ๏ธ
note that template strings also sometimes need a custom tag function, e.g.
lit`<html></html>`
how would you express this with #js "foo"
and how would you express interpolation?
I can help with async/await support in CLJS if you want @dnolenawesome! link for custom tag functions for me to read?
nvm found it
hrm ^lit #js "<html></html>" ... yeah needs thought. Thoughts welcome. But perhaps better in #cljs-dev
I think (js-template lit/html " giving lit.html`<h1>Hello ${name}</h1>` is reasonable ๐คทHello " name "
")
I agree that's better than what I'm suggesting ๐
in squint I have #html baked in, so you can write:
#html [:div "hello"]
and you can change the template function with
^lit #html [:div "hello"]
so yeah, I had that tag fn idea too ;)
But this is not a generic way of writing template strings. I did like the idea of being able to generate html via hiccup and then feed that to lit/html. Is this possible with js-template @thheller?Forgive me if the following is wrong, but it has been several years. But native async has lower overhead in V8: https://v8.dev/blog/fast-async
and it's tighter integration is what allows it to self capture traces during throw exceptions via (.-stack err) and .toString like if my Lambda crashes and it needs to log it.
My current solution I have used for years is have a custom mini version of Promesa, use Bluebird promises, override js/Promise and setup long stack traces via #?(:cljs (.config bluebird #js {:longStackTraces true}))
but obviously this is not performant, although it doesn't really matter for most CRUD style API request handling.
when using the V8 debugger, I do get the full stack traces, it just can't generate itself, unless real async is used. I'm just checking, but maybe my case is too niche.
@raymond.w.ko you can use squint or cherry today to do native async. not sure how big your lambda is and if it requires rewriting stuff. I'm volunteering to backport it to CLJS as well if David is ok with it. Not sure about the timeframe but I'm sure it's doable (given enough time).
thanks for the suggestion. i need to look into it. it's pretty huge like several MBs as it has a lot of JS libraries in it. i personally call them "fat" lambdas and it's not really in the spirit of how serverless lambdas are suppose to be.