{:deps {com.wsscode/async {:git/url "https://github.com/wilkerlucio/wsscode-async.git"
:sha "50ecd19d657980f2dd3c38b2df86a86983778145"}}
Core.async
is the standard way to handle async features in Clojure and ClojureScript programs.
Although core.async is built upon CSP, often (specially in CLJS) is desired to have something that’s more like Promises/Futures.
Core.async provides a promise-chan
, which is a channel that has a promise-like semantic:
after realization, any read on it will keep returning the same value. That’s helpful but
this doesn’t cover the problem of error propagation, in a promise system it’s expected
that errors can flow up to be captured by some higher level code.
This library provide some helpers to deal with this problem, and also:
helpers to integrate with Javascript Promises in ClojureScript environments.
helpers to write tests using core.async and cljs.test.
Currently supported only with deps:
{:deps {com.wsscode/async {:git/url "https://github.com/wilkerlucio/wsscode-async.git"
:sha "50ecd19d657980f2dd3c38b2df86a86983778145"}}
This library uses separated namespaces for Clojure and ClojureScript, when using the
Clojure side, use the namespace com.wsscode.async.async-clj
, and for ClojureScript
use com.wsscode.async.async-cljs
. Unless pointed otherwise, the features on both
namespaces are the same.
If you want to use this with Clojure Common files, you can use the require like this:
(ns my-ns
(:require [#?(:clj com.wsscode.common.async-clj
:cljs com.wsscode.common.async-cljs)
:as wa
:refer [go-promise <?]]))
To deal with error propagation, the trick is to return the error object as the channel value, but also throw that error when reading the channel. Let’s illustrate that with an example:
(ns my-ns
(:require [com.wsscode.async.async-clj :refer [go-promise <?]))
(defn async-divide [x y]
; (1)
(go-promise
(/ x y)))
(defn run [d]
(go-promise
(try
; (2)
(<? (async-divide 6 d))
(catch Throwable _
"ERROR"))))
(comment
(<!! (run 2)) ; => 3 (3)
(<!! (run 0)) ; => "ERROR" (4)
)
1 | go-promise is similar to go , but will return a promise-channel, so in case this result gets
passed to multiple readers, all of them will get the realized value or error. This also
wraps the block in a try/catch, so if some exception happens it will get returned as the channel value. |
2 | <? works like <! , but once it reads a value, it will check if it’s an error, and
if so it will throw that error, propagating it up on the chain. |
3 | propagate value back |
4 | error propagated from async-divide trying to divide by zero |
By following this pattern of error capture and send, this system ends up working in the
same ways you would expect a promise, all while still in the same go
blocks, making
it compatible with standard core.async functionality. Later we will also talk about how
to integrate with Javascript Promises in this system.
Another difference when using go-promise
is that different from regular go
, you can
return nil
as a value. Since the promise will always return the same value, a nil
is not ambiguous.
In the implementation side, go-promise check if the value returned is nil , and
if so it just closes the channel, making it an effective nil value.
|
We just saw the helper <?
to do a take and check for errors, here is a list of other
take helpers provided by this library:
<?
- take and throw errors
<!maybe
- if param is a channel, take from it, otherwise return its value
<!!maybe
(clj only) - like <!maybe
but blocking the thread
<?maybe
- take and throw errors, return value if it’s not a channel
in ClojureScript <?maybe can also take from Promises
|
<?!maybe
(clj only) - like <?maybe
but blocking the thread
While working in Javascript it’s common to need to handle Promises, to help with this
there is a macro in this library that enables the read of JS promises as if they
were core.async
channels, the <!p
helper:
(ns my-ns
(:require [com.wsscode.async.async-cljs :refer [go-promise <?]))
; (1)
(go-promise
(-> (js/fetch "/") <!p
(.text) <!p
js/console.log))
1 | Read the index text of the current domain, note we are waiting for two different promises in this example, the first one for the fetch headers and the second to get the body text. |
Note that this strategy allows the mixing of both core.async
channels and promises
in the same system, you can both park for channels or promises.
Dealing with async tests in cljs.test can be annoying, the core doesn’t have any integration
with core.async, neither it handles common problems like timing out a test. This library
provides a helper called deftest-async
that aims to facilitate the tests of async core
using core.async. Example usage:
(ns com.wsscode.async.async-cljs-test
(:require [clojure.test :refer [is are run-tests async testing deftest]]
[com.wsscode.async.async-cljs :as wa :refer [deftest-async <! go]]))
(deftest-async my-test
(is (= "foo" (<! (go "foo")))))
This macro will do a couple of things:
It will wrap the body in a go-promise
block, allowing the use of parking operations
Try/catch this block, if any error happens (sync or async) that generates a test case that will fail with that error
Add a 2 seconds timeout, if the go
block doesn’t return in this time it will cancel and fail the test
You can configure the timeout duration, example:
(ns com.wsscode.async.async-cljs-test
(:require [clojure.test :refer [is are run-tests async testing deftest]]
[com.wsscode.async.async-cljs :as wa :refer [deftest-async <! go]]))
(deftest-async my-test
{::wa/timeout 5000} ; 5 seconds timeout
(is (= "foo" (<! (go "foo")))))
There are other minor helpers not mentioned in this document, but they all have documentation on the functions, to check it out see the cljdoc page of this library.
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close