All Projects → filipesilva → async-interop

filipesilva / async-interop

Licence: MIT license
Use JS promises with core.async.

Programming Languages

clojure
4091 projects

Current status

Merged into CLJS, please refer to https://clojurescript.org/guides/promise-interop for the official guide.

Promise interop

Filipe Silva 2019-10-03 :type: guides :toc: macro :icons: font

Notes

This repository follows up on a discussion had on the Clojurians clojurescript slack channel where it was noted that questions about promises were a recurring topic.

David Nolen provided a macro (provided here almost verbatim) and remarked that it could be a candidate for the core.async.interop namespace along with a post on the topic.

This proposal is being tracked in https://clojure.atlassian.net/browse/ASYNC-230.

Meanwhile I’m putting this repository up with some tests and what I imagine could be a guide in https://clojurescript.org/guides/ (except for this "Notes" section). You can also get it on https://clojars.org/async-interop:

 :dependencies [[org.clojure/core.async "0.4.500"]
                [async-interop "0.1.4"]],
  (:require
   [cljs.core.async :refer [go]]
   [async-interop.interop :refer [<p!]])

Using JavaScript promises directly

Promises are a common way of handling asynchronous operations in JavaScript. You can just as easily use them in ClojureScript by calling the promise methods.

JavaScript:

Promise.resolve(42)
  .then(val => console.log(val));

ClojureScript:

(.then (js/Promise.resolve 42)
       #(js/console.log %))

However, chained promise methods in ClojureScript results in cascading code. Using the thread-first macro we can can get back to more elegant code.

JavaScript:

Promise.resolve(42)
  .then(val => console.log(val))
  .catch(err => console.log(err))
  .finally(() => console.log('cleanup'));

ClojureScript:

(.finally
  (.catch
  (.then (js/Promise.resolve 42)
          #(js/console.log %))
  #(js/console.log %))
  #(js/console.log "cleanup"))

; same as above
(-> (js/Promise.resolve 42)
    (.then #(js/console.log %))
    (.catch #(js/console.log %))
    (.finally #(js/console.log "cleanup")))

Promise-heavy code that uses await results in more complicated code structures that aren’t very friendly. Take this example from Puppeteer usage:

JavaScript:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  try {
    await page.goto('https://example.com');
    await page.screenshot({path: 'example.png'});
  } catch (err) {
    console.log(err);
  }

  await browser.close();
})();

ClojureScript:

(def puppeteer (js/require "puppeteer"))

(-> (.launch puppeteer)
    (.then (fn [browser]
             (-> (.newPage browser)
                 (.then (fn [page]
                          (-> (.goto page "https://clojure.org")
                              (.then #(.screenshot page #js{:path "screenshot.png"}))
                              (.catch #(js/console.log %))
                              (.then #(.close browser)))))))))

To tame this sort of code we turn to core.async.

Using Promises with core.async

ClojureScript offers excellent facilities for async programming in core.async. One especially handy tool is the <p! macro, that consumes a promise inside a go block.

Using go blocks allows us to write code that looks synchronous even though it’s actually asynchronous, exactly like await and async do in JavaScript.

ClojureScript:

(:require
   [cljs.core.async :refer [go]]
   [cljs.core.async.interop :refer-macros [<p!]])

(def puppeteer (js/require "puppeteer"))

(go
  (let [browser (<p! (.launch puppeteer))
        page (<p! (.newPage browser))]
    (try
      (<p! (.goto page "https://clojure.org"))
      (<p! (.screenshot page #js{:path "screenshot.png"}))
      (catch js/Error err (js/console.log (ex-cause err))))
    (.close browser)))

This is just scratching the surface. core.async gives you very powerful queue-like channels that can do much more than handle one-off promises.

You can read more about core-async in the repository, rationale, code walkthrough, and blog post.

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].