Chaining JS Promises in Clojurescript and the problem of the previous value
Intro
When writing asynchronous code in ClojureScript, or dealing with JavaScript libraries we often have to work with native JS Promises. If you’d like to make your code more readable and idiomatic, here’s a macro you could use:
(defmacro promise->
[promise & body]
`(.catch
(-> ~promise
~@(map (fn [expr] (list '.then expr)) body))
(fn [error#]
(prn "Promise rejected " {:error error#}))))
Put it in a previous.macros.clj
Clojure file and then in previous.macros.cljs
:
(ns previous.macros
(:require-macros [previous.macros]))
This allows you to :require
it like any other cljs namespace:
(ns previous.core
(:require [cljs.nodejs :as nodejs]
[previous.macros :refer [promise->]]))
Chained promises
Here is a basic example of using the macro to chain two promises:
(defn slow-promise []
(js/Promise. (fn [resolve reject]
(js/setTimeout #(do
(prn "I like slooow")
(resolve "slooow"))
1000))))
(defn fast-promise []
(js/Promise. (fn [resolve reject]
(do
(prn "I like fast!")
(resolve "fast!")))))
(promise-> (slow-promise)
fast-promise)
The output from evaluating the above code:
"I like slooow"
"I like fast!"
"fast!"
The fast-promise
had to wait with it’s execution for the slow-promise
, the return value printed is that of the last promise in the chain.
We will revisit this problem and talk about returning the previous value as well.
There is nothing stopping you from using the macro to evaluate nested Promise chains, and writing code that looks like this:
(promise-> (promise-> (js/Promise.resolve (prn "a"))
(js/Promise.resolve (prn "b")))
(js/Promise.resolve (prn "c")))
Handling Promise rejection
If one of the promises in the chain encounters an error, the macro handles that in its catch
block.
Here is an example:
(defn rejecting-promise []
(js/Promise. (fn [resolve reject]
(reject ":("))))
(promise-> (slow-promise)
rejecting-promise
fast-promise)
If you evaluate this code you would get:
"I like slooow"
"Promise rejected " {:error ":("}
Rejection was gracefully handled, app did not crash.
Chaining Promises with a previous value
What if you need to pass the data between the Promises in the chain?
One example would be a chain of calls to multiple servers, or simply a step-wise calculation.
In the previous example the return value of the slow-promise
was outside of scope for the fast-promise
.
One simple solution is to use a map and cumulatively add values to it:
(defn promise-1 [previous]
(js/Promise.resolve (assoc previous :val1 1)))
(defn promise-2 [{:keys [:val1] :as previous}]
(js/Promise.resolve (assoc previous :val2 (inc val1))))
(promise-> (js/Promise.resolve {})
#(promise-1 %)
#(promise-2 %)
prn)
Result:
{:val1 1, :val2 2}
Collection of Promises
If we need to wait untill all of the promises are resolved we can use the macro in combination with Promise.all
, passing a collection of Promise objects:
(promise-> (js/Promise.all
[(js/Promise.resolve "FU")
(js/Promise.resolve "BAR")])
prn)
Result:
["FU" "BAR"]
From a callback to a Promise
Finally, what if some asynchronous API works with callbacks, rather that JS Promises? We can easily convert a callback-style code into a Promise based one:
(defn return []
"result")
(defn i-like-callback [callback]
(callback (return)))
(defn i-like-promise []
(js/Promise. (fn [resolve reject]
(i-like-callback resolve))))
(promise-> (i-like-promise)
prn)