# Clojurescript

> Write, review, debug, and configure ClojureScript code and projects. Use this whenever the user mentions ClojureScript, .cljs/.cljc files, shadow-cljs, figwheel, the CLJS compiler, JS interop from Clojure, Reagent/re-frame/Reagent-style UI code, or asks to convert JS/TS logic into ClojureScript. Also use it for questions about CLJS compiler options (:optimizations, :main, :npm-deps, :externs, etc.), consuming JS/npm libraries from CLJS, source maps, Google Closure Library usage, or the newer ^:async/await function support. Also trigger for CLJS recursion/stack-overflow/trampoline questions. Trigger even if the user just pastes CLJS code with an error and asks "what's wrong here" or asks to set up a new CLJS project. Do NOT use for nbb (babashka/nbb) scripts, `nbb.edn` projects, or anything meant to run via `nbb script.cljs`/`npx nbb` — those have a different (SCI-interpreted, no-Closure-Compiler) language surface; those are out of scope for this skill — consult nbb's own documentation.

- Skill: `bsene/clojurescript` (Agent Skill, multi-file: 6 files)
- Install (CLI): `npx skillmds@latest add bsene/clojurescript`
- Raw SKILL.md: https://api.skillmd.com/api/skills/bsene/clojurescript/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Docs & Writing
- Author: bsene (https://skillmd.com/u/bsene)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/bsene/clojurescript

---


# ClojureScript

ClojureScript (CLJS) is Clojure that compiles to JavaScript via the Google Closure Compiler. It shares Clojure's syntax, immutable data structures, and REPL-driven workflow, but runs on JS engines (browser, Node.js) and interoperates directly with JS objects, functions, and npm/Closure libraries.

This skill covers writing idiomatic CLJS, JS interop, dependency/build configuration, and the newer native async/await support. When in doubt about compiler flags or interop mechanics, check `references/` rather than guessing — CLJS has a lot of build-tooling surface area that's easy to get subtly wrong.

> **Not for nbb.** [Babashka/nbb](https://github.com/babashka/nbb) runs CLJS through an SCI interpreter (no Google Closure Compiler, no `:optimizations`/`:advanced`, no externs) for fast-starting Node.js scripts — a different language surface from what this skill covers. nbb is out of scope here — consult [nbb's docs](https://github.com/babashka/nbb) and [examples](https://github.com/babashka/nbb/tree/main/examples) for `nbb.edn` projects, anything run via `nbb script.cljs`/`npx nbb`.

## Gotchas

- **`:optimizations` defaults to `:none`**, not `:advanced` — a project with no explicit `:optimizations` setting is running unoptimized dev output.
- **Symbol renaming under `:advanced` breaks untyped JS interop.** "Works in dev, breaks in prod" is almost always this — fix with an externs file or string-keyed access (`goog.object/get`), not by changing app logic.
- **`:main` under `:optimizations :none` only loads what's actually `:require`d.** A side-effect-only namespace not reached by the entry namespace's transitive requires silently doesn't run — add an explicit `:require` or put it in `:preloads`.
- **`:output-to` and `:modules` are mutually exclusive**, not layered — `:modules` needs a per-module `:output-to` inside each module map, not a top-level one.
- **No arbitrary-precision numbers.** CLJS numbers are all JS `number` (a double) — no JVM-style BigDecimal/BigInt/ratios. Watch for precision loss on large integers.
- **`^:async` metadata goes on the `fn`/name only, never the arg vector**, and `await` only works inside a function actually marked `:async` — a nested `fn` doesn't inherit it and needs its own `^:async` (see `references/async-functions.md`).
- **An `:async` function always returns a `Promise`**, even when the body looks like it returns a plain value.
- **Truthiness differs from JS.** Only `false` and `nil` are falsy — `0`, `""`, `js/NaN`, `[]`, and `(array)` are all truthy. `(if 0 "yes" "no")` → `"yes"`, unlike JS. Easy to get backwards when translating JS/TS conditionals.
- **No automatic tail-call optimization, same as JVM Clojure.** CLJS functions run as ordinary JS function calls, and most JS engines don't reliably implement proper tail calls either — deep non-tail self-recursion will blow the JS call stack on large/unbounded input, even though it "works" for small test inputs. Reach for `recur` (self-recursion, tail position only — see below), `trampoline` (mutual recursion), or wrap the recursive case in `lazy-seq` (sequence-producing recursion) rather than plain recursive calls whenever the input size isn't small and fixed.

## Core language cheat sheet

CLJS syntax is essentially Clojure's, so standard Clojure knowledge (immutable persistent data structures, `let`/`fn`/`defn`, `->`/`->>` threading, `map`/`filter`/`reduce`, multimethods, protocols, `core.async`) applies directly. For a categorized index of core-library functions when you need the exact name of something less common, see `references/core-function-index.md` (based on https://cljs.info/cheatsheet/). The CLJS-specific things to keep front of mind:

- **Numbers**: CLJS has only one numeric type backed by JS `number` (a double). No arbitrary-precision integers/ratios like JVM Clojure — watch for precision issues with large integers.
- **Namespaces map to JS modules/Closure namespaces.** `(ns my.app.core (:require [my.app.util :as u]))` — `:require` for CLJS/Closure namespaces, `:import` for Closure classes/enums (e.g. `(:import [goog Timer])`).
- **`defprotocol`/`deftype`/`defrecord`** work like Clojure and compile to real JS constructor functions/prototypes.
- **`js/` prefix** accesses JS global objects: `js/console`, `js/document`, `js/Math`, `js/window`.
- **No JVM interop** — anything JVM-specific (`Thread`, `java.*` classes, blocking IO) doesn't exist. Concurrency is single-threaded/event-loop based; use `core.async` channels or promises/async functions instead of threads or locks.
- **`.cljc` files** hold code shared between Clojure and ClojureScript, guarded with reader conditionals: `#?(:clj (do-jvm-thing) :cljs (do-js-thing))`.

## JavaScript interop

- Method call: `(.methodName obj arg1 arg2)` → `obj.methodName(arg1, arg2)`
- Property access: `(.-propName obj)` → `obj.propName`
- Property set: `(set! (.-propName obj) val)`
- Constructor: `(SomeClass. arg1 arg2)` → `new SomeClass(arg1, arg2)`
- Static/global access: `js/JSON.stringify`, `js/Object.keys`
- Chained calls read top-down with `..`: `(.. js/document (getElementById "app") -innerHTML)`
- JS↔CLJS data conversion: `(clj->js m)` and `(js->clj o :keywordize-keys true)`
- Destructuring JS objects: `(let [{:keys [a b]} (js->clj obj :keywordize-keys true)] ...)` or interop-style `^js` type hints with `.-` accessors when you don't want a full conversion.

For consuming Closure Library, npm packages, foreign (non-Closure-compatible) JS, and CLJS libraries — including externs, advanced-compilation pitfalls, and CLJSJS — see `references/dependencies-and-interop.md` before guessing at a `:require`/`:import` shape or writing an externs file from scratch.

## Recursion

Because CLJS has no automatic TCO (see Gotchas), which recursion tool to reach for depends on the shape of the problem:

- **`recur`** — self-recursion only (a function calling itself, directly, not through an intermediate function), and only from the tail position. Compiles to a real loop, so it's stack-safe for arbitrarily large input. This is the default choice for producing a scalar or a small, fixed-size result — e.g. summing a collection with `loop`/`recur` rather than a plain recursive call.
- **`trampoline`** — for **mutual** recursion (function A calls B calls A), where `recur` doesn't apply because it only optimizes self-calls. Wrap each recursive tail call in `#(...)` and drive the whole thing with `(trampoline f args...)`; `trampoline` keeps calling the returned function until it gets back a non-function value. Don't reach for `trampoline` on a self-recursion — `recur` is simpler and equally stack-safe there.
- **`lazy-seq`** — for recursion that produces a sequence, especially an unbounded/large one. Wrapping the recursive call in `lazy-seq` breaks the recursion into on-demand steps, so callers only pay for the elements they actually realize (`take`, `first`, etc.). This is usually the better fit over `loop`/`recur` whenever the output size varies or could be large — let `take`/`drop` on the caller side decide how much to compute.
- **`memoize`** — trades space for time by caching by argument, useful for expensive recursive/mutually-recursive definitions with overlapping subproblems. Caveat: memoizing alone doesn't fix a stack-overflow risk — a cold cache still has to recurse all the way down on the first large call. If the recursion is deep, build the cache bottom-up by mapping the memoized function over a lazy range/seq (`(map f (range))`) rather than calling it directly on a large `n`.

## Async / promises

CLJS added native `^:async` functions with an `await` macro (since v1.12.145) as a lighter-weight alternative to `core.async` for promise-based code. This is a newer feature that's easy to get wrong or not know about at all — **read `references/async-functions.md` before writing or reviewing any CLJS code that deals with `Promise`s, `fetch`, or `async`/`await`-shaped logic** (see Gotchas above for the two most common mistakes).

The single most common typo — `^:async` sits on the function **name**, never the argument vector:

````clojure
(defn ^:async load-user [id] (await (fetch-user id)))  ; ✅ name
(defn load-user ^:async [id] ...)                      ; ❌ arg vector — never
``` One more rule worth holding in mind: prefer `Promise/all` (via `mapv`) over `map` when you need to await several promises produced in a loop.

If the codebase already uses `core.async` (`go`, `<!`, channels) for async control flow, stay consistent with that style rather than mixing in `^:async`/`await` unless asked to migrate.

## External resources

For idiomatic-Clojure background/mindset reading (design patterns translated to Clojure idioms, `core.async` worked examples, FP mindset shifts) beyond this skill's own reference material, plus community-curated library lists and real-world example codebases, see `references/external-resources.md`. Use it as supplementary color/perspective and pointers, not as the source of truth for CLJS-specific mechanics — those stay in this skill's other reference files.

## Build & compiler configuration

CLJS projects are almost always built with **shadow-cljs** (npm/JS-ecosystem friendly, handles npm deps automatically) or the **Clojure CLI (`deps.edn`) + `cljs.main`/`cljs-build-api`** combo; some older projects use Leiningen + `figwheel-main`. Don't assume the tool — check for `shadow-cljs.edn`, `deps.edn`, or `project.clj` in the project before giving setup instructions.

For the full list of compiler options (`:optimizations`, `:target`, `:main`, `:npm-deps`, `:modules`, `:source-map`, `:closure-defines`, warnings, etc.) with exact semantics and defaults, see `references/compiler-options.md`. Don't guess at option names or defaults — this is a large, easy-to-misremember surface (e.g. `:static-fns` defaults to `false` except under `:advanced`, `:npm-deps` defaults to `false`). See Gotchas above for the most common failure patterns tied to these options.

## Working style

**Default to idiomatic Clojure(Script), not a literal JS translation.** This is the top priority whenever writing or rewriting CLJS, and it should win over "closest to what the JS/TS looked like" unless the user's existing codebase clearly does otherwise. Concretely, that means:

- **Immutability and pure functions first.** Reach for persistent data structures (maps, vectors, sets) and pure transformations over mutable state, `atom`s, or `set!`. Only use an `atom`/`volatile!` for genuine local mutable state (e.g. component-local UI state), not as a default variable substitute.
- **Data over classes.** Model domain data as plain maps/records rather than JS-style classes with methods, unless polymorphism genuinely calls for `defprotocol`/`deftype`/`defrecord` or a JS class is required at an interop boundary (e.g. a React component).
- **Sequence/threading idioms over loops.** Prefer `map`/`filter`/`reduce`/`keep`/`for` and `->`/`->>` threading over `for`/`while`-style imperative loops, index juggling, or manual accumulation. This is a style preference, not a stack-safety rule: `loop`/`recur` is the *correct* (not just a perf-justified) choice for genuinely tail-recursive, scalar-producing logic — see Recursion above — reach for a transient only when profiling justifies it.
- **Destructuring over manual accessors.** Use `let`/fn-arg destructuring (`{:keys [...]}`, `[a b & rest]`) instead of repeated `(get m :k)` or positional indexing.
- **Small composed functions over long procedural bodies.** Break logic into small, named, testable functions and compose them, rather than one large function with sequential mutation-heavy steps.
- **Idiomatic control flow.** Prefer `cond`/`case`/`condp` and `when`/`if-let`/`when-let` over nested `if`s or JS-style early-return chains; prefer `^:async`/`await` or `core.async` (per the async section above) over manual `.then` chains.
- **Namespaced keywords and data-driven design** where they fit naturally (e.g. `:person/name` in shared/library code) rather than plain strings for map keys.

When converting JS/TS to CLJS specifically: re-derive the idiomatic CLJS shape of the logic rather than transliterating line-by-line — e.g. turn `.then` chains into `^:async`/`await` or `core.async`, turn classes into `defrecord`/protocols or plain maps + functions, turn `for`-loops with accumulator variables into `reduce` or `into`.

If the surrounding codebase has an established (even if less idiomatic) convention, match it for consistency rather than unilaterally "fixing" the whole file — but say so, and offer the idiomatic alternative as a suggestion rather than silently picking one or the other.

Other working-style notes:
- When debugging, ask whether the error is happening under `:none` (dev) or `:advanced`/`:simple` (prod) optimizations — the failure modes are very different (the former is usually a logic/require bug, the latter is very often a renaming/externs issue).

---


## Benchmark

Scenario: `.benchmarks/scenarios/clojurescript-001-async-await.md` · Run: 2026-08-31 (salience re-run `wf_9a5588bc`) · Log: `.benchmarks/runs/2026-08-31/clojurescript-001-async-await.json`

| Model             | Without | With  | Delta |
| ----------------- | ------- | ----- | ----- |
| claude-opus-4-8   | 67%     | 100%    | +33%   |
| claude-sonnet-4-6 | 83%     | 100%    | +17%   |
| claude-haiku-4-5  | 100%    | 100%    | +0%   |

> **PASS (run 2026-08-31)**. Salience re-run (✅/❌ `^:async`-placement pair added to the Async section, wf_9a5588bc): sonnet's arg-vector typo cleared (+17); gains on all models. The prior single-run sonnet −17 was a salience gap, not noise. Gate per `.agents/skills/skill-optimizer/rules/release-gates.md`.
````

