Exporting ClojureScript Functions to JavaScript with ^:export
Learn how to expose ClojureScript functions to plain JavaScript using ^:export, see a concrete example, and understand the trade‑offs with dead‑code elimination.
02 Sept 2026, 11:27 UTC

The problem: calling ClojureScript from existing JavaScript
When a project already ships JavaScript bundles, adding a ClojureScript module often means writing a wrapper or using advanced build tools to expose functions. The ^:export metadata offers a lightweight way to make a ClojureScript var reachable from plain JavaScript without extra boilerplate.
How ^:export works
The metadata tells the ClojureScript compiler to generate a JavaScript property whose name matches the qualified var (e.g., myapp.api/add becomes myapp.api.add) on the global object or the module’s exports, depending on the build target. The emitted code is essentially a simple assignment that survives dead‑code elimination only if the compiler determines the var is needed.
Worked example
(ns myapp.api
^:export
(defn add [a b]
(+ a b)))
The function receives JavaScript numbers directly and returns a number, so no conversion is needed in this case. If the function dealt with ClojureScript collections, you would typically wrap the call with js->clj or clj->js at the boundary.
Build configuration
For a shadow‑cljs project, a minimal shadow-cljs.edn can look like:
{:builds
{:app
{:target :browser
:output-dir "public/js"
:asset-path "/js"
:modules {:main {:init-fn myapp.api/init}}}}}
Running shadow-cljs watch app (or release with :optimizations :advanced) produces a file public/js/app.js. In the output you will see a line similar to:
myapp.api.add = function(a,b){return a+b;}
If you compile with :advanced and do not require the namespace from JavaScript, the compiler may drop the assignment because it believes the var is unused. To prevent that, either:
- Require the namespace from JavaScript via
goog.require('myapp.api')(orimport * as myappApi from './app.js'when using ES‑modules), or - Add an explicit
:exportflag in the compiler options (e.g.,:exportunder:compiler-options) which tells the compiler to keep the var regardless of usage.
Trade‑offs and limitations
The primary trade‑off is global namespace pollution: each exported var creates a property on window (or the module’s export object) using the qualified name. In large codebases this can increase the risk of collisions and feels less idiomatic than a dedicated ES‑module export. Additionally, ^:export does not automatically convert ClojureScript data structures; callers must handle interop themselves, which can be easy to forget.
A common mitigation is to create a thin “api” namespace that only contains ^:export functions delegating to internal implementation namespaces. This keeps the public surface small and lets you change internals without breaking JavaScript callers.
Checking the result
After building, open the generated JavaScript file and search for the exported name (e.g., myapp.api.add). Then load the file in a browser console or Node.js and call the function:
console.log(myapp.api.add(2,3)); // should print 5
If the line is missing, rebuild with the namespace explicitly required from JavaScript or add the compiler export flag, then verify again.
By following these steps you can safely expose selected ClojureScript logic to existing JavaScript toolchains while being aware of the dead‑code elimination and namespace considerations.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.