(-> (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]))))
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
Note that there's nothing special about the use of
+ is a valid symbol character so it can be part of function and variable names.
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:
mapand it just works as expected.
[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.
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)))))))))
Consider this function:
(defn test-1+  (-> (bar+ (foo)) (.then (fn [result] (baz result)))))
What happens when things go wrong?
bar+fails asynchronously, i.e. if the promise returned by
bar+is rejected, the promise returned by
test-1+will also be rejected.
bazthrows synchronously, the promise returned by
test-1+will also be rejected.
So far so good. But
test-1+will throw an exception instead of returning a promise; and
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
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.