Composing Asynchronous UI in ClojureScript with core.async: A Practical Guide
Discover how core.async’s go blocks and channels simplify async UI workflows in ClojureScript, compare to callbacks, see a concrete example and practical limits.
11 Oct 2025, 22:01 UTC

Why callbacks still trip up ClojureScript UI code
When a button click triggers a network request, the typical pattern in ClojureScript looks like this:
(defn fetch-data []
(http/get "https://example.com/api"
(fn [response]
(when (= 200 (:status response))
(let [data (:body response)]
(js/set innerHTML (select "#result" js/document) (pr-str data))))))
Each new asynchronous step adds another nested function, quickly making the flow hard to read. The problem is not just visual clutter; deep nesting can hide errors and make debugging painful.
The core.async advantage
core.async treats asynchronous operations as first‑class values. A go block is a lightweight thread that the ClojureScript compiler turns into a JavaScript generator. A channel is an immutable, thread‑safe conduit that lets different parts of the program exchange values safely.
Because go blocks schedule themselves on the JavaScript event loop, you don’t need to manually manage timers or requestAnimationFrame calls to keep the UI responsive.
Concrete example: a simple async pipeline
Let’s build a minimal REPL example that fetches a number, waits a second, and prints it.
- Create a channel that will carry the number.
- Start a go block that puts the number after a delay.
- Another go block takes the value and logs it.
(require '[clojure.core.async :as async])
(def nchan (async/chan 1)) ; a single‑item channel
(async/go
(async/
Run this in a ClojureScript REPL (e.g., shadow-cljs watch app). You should see “received: 42” appear after roughly one second, even though the main thread never blocks.
To verify the generated JavaScript, inspect the compiled output in target/js/src. You will find a function that uses yield statements, confirming that no extra callback wrappers were added.
How this translates to real UI work
Imagine a component that loads a user profile, updates the avatar, and then enables a “Save” button.
- Create a channel for the profile data.
- Start a go block that performs the HTTP request and puts the parsed JSON into the channel.
- Another go block watches the channel, updates the DOM, and then puts a “ready” signal onto a second channel.
- The click handler for “Save” takes the ready signal, sends the new data back through a channel, and a final go block applies the changes.
Because each step is isolated in its own go block, the code reads like a straight‑line sequence, yet the UI stays responsive throughout.
Trade‑offs and practical limits
Learning the CSP (Communicating Sequential Processes) model takes time. Developers accustomed to simple callbacks may find the ! and put! syntax unfamiliar.
Memory usage can rise if you create many channels or long‑living go blocks; the browser’s garbage collector must track each channel’s state. In a large app, this can affect performance, especially on low‑end devices.
Interoperability with existing JavaScript libraries often requires manual conversion of callbacks into channels, adding boilerplate. For example, a jQuery plugin that uses event callbacks must be wrapped in a go block that listens to the plugin’s events and pushes them onto a channel.
Actionable next steps
1. Set up a REPL: install shadow-cljs, start a watch build, and open the REPL.
2. Run the minimal example above, observe the delayed log, and confirm the async behavior.
3. Inspect compiled JavaScript: locate the generator function and verify that the async flow is expressed with yield rather than nested callbacks.
4. Write a unit test that asserts the order of values received from a channel, using go to drive the test and cljs.test for assertions.
Following these steps will let you evaluate whether core.async fits your project’s concurrency needs, and you’ll have concrete evidence of its impact on code clarity and UI responsiveness.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.