--- name: clojurescript description: Write, review, debug, and configure JVM Clojure and ClojureScript code and projects. Use for .clj/.cljc/.cljs files, deps.edn, the Clojure CLI, Leiningen, REPL-driven development, Java or JavaScript interop, shadow-cljs, figwheel, the CLJS compiler, Reagent/re-frame, and CLJS compiler options. Do not use for Babashka or nbb scripts. compatibility: No special tools required to consult this skill. Actually building or running code needs a CLJS toolchain (shadow-cljs, Clojure CLI, or Leiningen) and a JS runtime (browser or Node.js), but the skill itself is pure reference/instruction content. metadata: version: "1.0.0" source: https://clojurescript.org/reference/documentation --- # Clojure & ClojureScript Clojure runs on the JVM. ClojureScript (CLJS) shares its syntax, immutable data structures, and REPL-driven workflow, but compiles to JavaScript via the Google Closure Compiler and interoperates with JS objects, functions, and npm/Closure libraries. This skill covers idiomatic Clojure, JVM/JS interop, dependency/build configuration, and native CLJS async/await. First identify the runtime: `.clj` is JVM Clojure, `.cljs` is ClojureScript, and `.cljc` needs a deliberate shared-runtime boundary. When in doubt about compiler flags or interop mechanics, check `references/` rather than guessing. > **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))`. ## JVM Clojure For `.clj` code, prefer the official Clojure CLI and `deps.edn` for a new project, but preserve an existing Leiningen or build-tool workflow. Common checks are `clj` for a REPL, `clojure -M -m my.app.main` for a main namespace, and `clojure -X:deps tree` for resolved dependencies. ### Explore and shape data - Start with the REPL for every non-trivial change: evaluate a small input, inspect the returned data, then put the confirmed transformation in a named function. Use `*1`, `*2`, `*3`, and `*e` to inspect recent results and failures. - Model domains with maps, vectors, sets, and namespaced keywords. Destructure function inputs instead of repeatedly navigating maps, and return data rather than hiding it in objects. - Compose collection transformations with `map`, `filter`, `keep`, `reduce`, `into`, `group-by`, and `mapcat`. Use `->` when the working value is the first argument and `->>` when it is the last; use `cond->`/`cond->>` for optional steps. - Do not use lazy sequences for effects. Use `run!`, `doseq`, or an eager collection operation when effects must occur. Realize a lazy sequence while its resource is open: ```clojure (with-open [reader (clojure.java.io/reader path)] (doall (line-seq reader))) ``` - Use `loop`/`recur` for deep tail-recursive scalar work. JVM recursion is not stack safe; use `lazy-seq` when the result itself is incremental or unbounded. ### Organize and verify behavior - Give each file a namespace and use `:require` aliases (`[my.app.orders :as orders]`) to make a function's origin clear. Avoid broad `:refer` imports outside narrowly justified cases. - Use `clojure.test` for focused behavior checks: name the behavior with `deftest`, state outcomes with `is`, and add a `testing` label when it makes failures clearer. Test pure transformations directly; test I/O through a thin adapter. - Validate untrusted input at its boundary with an explicit predicate or `clojure.spec.alpha`; return or report data that identifies the bad field. Do not spread validation checks throughout downstream transformations. - Use `ex-info` with useful `ex-data` for expected domain failures; preserve causes when wrapping exceptions. ### State, polymorphism, and metaprogramming - Default to no shared mutable state. Use an `atom` with `swap!` for one independent identity; never split a read/compute/write update across `@` and `reset!`. Use `ref` and `dosync` only for coordinated, retry-safe changes to several identities. - Keep Java interop behind a thin adapter. Add type hints only for a demonstrated reflection or overload-resolution issue. - Prefer maps and functions. Introduce a protocol for a genuine polymorphic boundary, a multimethod when dispatch depends on more than one attribute, and a macro only when a function cannot provide the required evaluation control. Inspect a macro with `macroexpand-1` before relying on it. These practices are distilled from the supplied *Clojure* (Karthikeyan A.K.), *Programming Clojure*, Second Edition (Stuart Halloway and Aaron Bedra), and *The Clojure Workshop* (Joseph Fahey et al.). Verify modern tooling details against the official Clojure documentation. ## 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`, ``/`->>` 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`. ````