Promise handling in CLJS using js-await

Recently I had to write some code using Promises and thought it was time for some syntax sugar. So I added a very simplistic macro to basically emulate what JS await does while actually mapping to traditional Promise .then and .catch. This way the macro can stay extremely simple without having to modify the compiler to add support for async functions and actual await. That is a quest for another day.

Currently this is only available as part of shadow-cljs (since 2.19.1) but the macro you can easily copy to a separate namespace and use anywhere.

The use is very simple. You get it by requiring [shadow.cljs.modern :refer (js-await)] in your ns or REPL.

(defn my-async-fn [foo]
  (js-await [the-result (promise-producing-call foo)]
    (doing-something-with-the-result the-result)
    (catch failure
      (prn [:oh-oh failure])))

Without the js-await this was (and is what the macro will emit)

(defn my-async-fn [foo]
  (-> (promise-producing-call foo)
      (.then (fn [the-result]
               (do-something-with-the-result the-result)))
      (.catch (fn [failure]
                (prn [:oh-oh failure])))))

Also sort of fine but gets a little verbose.

Both map pretty much to this JS

function my_async_fn(foo) {
  promise_producing_call(foo)
    .then(function(the_result) {
      return doing_something_with_the_result(the_result);
   })
   .catch(function(failure) {
     console.log("oh-oh", failure)
   })
}

which more or less would map to actual async/await.

async function my_async_fn(foo) {
  try {
    let the_result = await promise_producing_call(foo);
    return doing_something_with_the_result(the_result);
  } catch (failure) {
    console.log("oh-oh", failure);
  }
}

catch is optional, but same as try if it is the last form of the body it will be used for the .catch or a promise.

(defn my-async-fn [foo]
  (js-await [the-result (promise-producing-call foo)]
    (doing-something-with-the-result the-result))

Await can be nested as long as it is the last form in the body the promise chaining will just work.

(defn my-fetch-fn []
  (js-await [res (js/fetch "/some.json")]
    (js/console.log "res" res)
    (js-await [body (.json res)]
      (js/console.log "got some json" body))))

(js/console.log "my-fetch-fn" (my-fetch-fn))

The syntax is just

(js-await [binding promise-thing-or-call]
   ... body)

Note that js-await always returns a promise. There is currently no error checking of any kind so things will break if the promise-thing-or-call doesn’t actually return a promise (or strictly speaking a “thenable”). binding can use destructuring so {:keys [foo] :as x} is valid. Only a single binding-pair is valid, everything else will be discarded. There are also no checks for that so beware.

Thats it really. I just wanted some extremely basic syntax sugar since nesting some .then calls got a little annoying otherwise. I’m aware of alternative solutions but they do too much for my taste.

Maybe this is useful for someone else.

11 Likes

Thanks Thomas, looks very useful to me!

There are lots of similar macros available in promesa and kitchen-async.

The equivalent in those libraries is p/let + p/catch:

$ nbb
Welcome to nbb v0.5.103!
user=> (require '[promesa.core :as p])
nil
user=> (def x<> (p/let [x (p/delay 1000 :hello)] x))
#'user/x<>
user=> (.then x<> prn)
:hello
#<Promise[~]>

user=> (def x<> (-> (p/do (assoc :foo :bar :baz)) (p/catch (fn [_] :error)))
)
#'user/x<>
user=> (.then x<> prn)
:error
#<Promise[~]>

Yes, those I was referring to in my post.

However, they are all similar in that they deal with Promises yes. Similar in scope and the amount of code they generate no. They are totally viable though and have a lot more features, no need to use js-await if they work for you.

Since its all based on .then and .catch they all work totally fine together too.

1 Like

I might have missed that. Which post are you referring to in which you refer to other similar macros?

A side-note: nbb also supports a top-level await (mostly for REPL convenience, I see no need for it in production code). Together with these macros, there is hardly any need to support async/await in the language:

user=> (nbb.core/await (p/delay 1000 :hello))
:hello
1 Like

I’m aware of alternative solutions but they do too much for my taste.

Oh, I see! Makes sense.