Skip to content

Instantly share code, notes, and snippets.

@pesterhazy
Last active June 10, 2024 23:37
Show Gist options
  • Save pesterhazy/c4bab748214d2d59883e05339ce22a0f to your computer and use it in GitHub Desktop.
Save pesterhazy/c4bab748214d2d59883e05339ce22a0f to your computer and use it in GitHub Desktop.
Promises in ClojureScript

Chaining promises

Chaining promises in ClojureScript is best done using the thread-first macro, ->. Here's an example of using the fetch API:

(-> (js/fetch "/data")
    (.then (fn [r]
             (when-not (.-ok r)
               (throw (js/Error. "Could not fetch /data")))
             (.json r)))
    (.then (fn [r]
             (prn [:result r])))
    (.catch (fn [e]
              (prn [:error e]))))

Marking functions as asynchronous

It can sometimes be hard to tell just by looking at a function whether it's synchronous (it returns a value) or asynchronous (it returns a promise). A convention can help:

(defn fetch-data+ []
  (-> (js/fetch "/data")
      (.then (fn [r]
               (when-not (.-ok r)
                 (throw (js/Error. "Could not fetch /data")))
               (.json r)))))

We're using the + suffix to indicate that fetch-data+ is an asynchronous function, i.e. that the function returns a promise. With this convention, you can see at a glance whether a given function is async. If so, you often need to add a .then and .catch handler.

Note that there's nothing special about the use of + here. In Clojure and ClojureScript (but not in JavaScript), + is a valid symbol character so it can be part of function and variable names.

Parallel execution

To run multiple asynchronous processes in parallel, use Promise.all:

(-> (->> ["/a" "/b" "/c"]
         (map (fn [url] (-> (js/fetch url)
                            (.then (fn [r] (.json r))))))
         js/Promise.all)
    (.then (fn [[a b c]]
             (prn [:a a])
             (prn [:b b])
             (prn [:c c]))))

Promise.all waits for all component promises to complete. When any of the promises rejects, the compound promise will reject with the same value. If instead of waiting for all promises you want execution to continue when any of the promises resolves, use Promise.race, which is available in all modern browsers.

A few things to note about data structures:

  1. Promise.all accepts a JavaScript array of promises or values. But instead of an array it can also take any JavaScript iterable as an argument. As a result, we can simply pass in a ClojureScript vector or, as in this example, the lazy sequence returned by map and it just works as expected.

  2. The promise returned by Promise.all resolves to a JavaScript array of values. In our then handler, we destructure this JavaScript array using [a b c] as if we were destructuring a ClojureScript vector. That also just works because destructuring internally uses clojure.core/nth, which operates on arrays as well as on vectors.

Asynchronous conditionals

When a function conditionally returns a result synchronously or asynchronously, there's a trap to avoid. Consider a function that counts the number of characters in a response:

(defn count-1+ [url]
  ;; *** INCORRECT: returns number or promise
  (if (empty? url)
    0
    (-> (js/fetch url)
        (.then (fn [r] (.text r)))
        (.then (fn [r] (count r))))))

This function contains a bug. Even though we use the + suffix to indicate that the function is async, it doesn't actually return a promise in the first branch of the conditional. The most elegant fix is to wrap the entire conditional in Promise.resolve:

(defn count-2+ [url]
  ;; *** CORRECT
  (js/Promise.resolve
   (if (empty? url)
     0
     (-> (js/fetch url)
        (.then (fn [r] (.text r)))
        (.then (fn [r] (count r)))))))

That works because Promise.resolve takes either a value or a promise of a value as an argument and always returns a promise.

Equivalently, you can use the Promise constructor:

(defn count-3+ [url]
  ;; *** CORRECT
  (js/Promise. (fn [resolve _reject]
                 (resolve
                  (if (empty? url)
                    0
                    (-> (js/fetch url)
                        (.then (fn [r] (.text r)))
                        (.then (fn [r] (count r)))))))))

Two failure modes

Consider this function:

(defn test-1+ []
  (-> (bar+ (foo))
      (.then (fn [result]
               (baz result)))))

What happens when things go wrong?

  • If bar+ fails asynchronously, i.e. if the promise returned by bar+ is rejected, the promise returned by test-1+ will also be rejected.
  • If baz throws synchronously, the promise returned by test-1+ will also be rejected.

So far so good. But

  • if foo throws, test-1+ will throw an exception instead of returning a promise; and
  • if bar+ fails synchronously, test-1+ will also throw an exception.

In other words, test-1+ can fail in two different ways, synchronously or asynchronously.

When this is not desirable, the empty resolve pattern can be used:

(defn test-2+ []
  (-> (js/Promise.resolve)
      (.then (fn []
               (bar+ (foo))))
      (.then (fn [result]
               (baz result)))))

Rewritten in this way, test-2+ will always return a promise, regardless of the behavior of the foo and bar+. There's only one failure mode. If any call inside test-2+ fails, it will fail by rejecting promise. Internally, the reason why that happens is because everything we do in test-2+ is wrapped in an anonymous function with an implicit try/catch block that converts exceptions into promise rejections.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment