ClojureScript Release Build Breaks but Dev Works: Diagnosing :advanced Compilation Failures
Dev build works, release build throws 'undefined is not a function'? Diagnose ClojureScript :advanced compilation property renaming with pseudo-names, externs, and goog.object fixes.
05 Oct 2026, 17:52 UTC

Your ClojureScript app runs perfectly in development — hot reload, REPL, everything. You build the release bundle, deploy it, and the browser console shows undefined is not a function or x.foo is not a function, or worse, a screen renders with silently missing data. The useful takeaway: this is almost always a property-renaming problem in Google Closure Compiler's :advanced mode, and you can pin down the exact renamed symbol in one rebuild using :pseudo-names true.
Why the release build behaves differently
Development builds use :optimizations :none, which keeps every name intact. Release builds typically use :advanced, where the Closure Compiler renames local variables and object properties to short symbols like a.b to shrink output and enable dead-code elimination.
The compiler can safely rename anything it fully understands — pure ClojureScript code. It cannot safely rename property access on plain JavaScript objects it doesn't control: interop calls like (.-name js-obj), calls into npm libraries, or string-keyed dynamic access. Unless you declare those names via externs or type hints, the call site gets renamed while the actual object still has the original property — and the lookup fails.
Symptom-to-cause table
| Symptom | Likely cause |
|---|---|
Error mentions a short mangled name (a.bc is not a function) | Renamed JS interop property at the call site |
| Failure appears only in the release build | Optimization-level difference; suspect renaming, not logic |
| Failure traces to one npm dependency | Missing externs for that library's API surface |
| No exception, but data is missing or UI is blank | Property read silently returns undefined after rename |
| Failure only with a plain (non-npm) JS file you included | No externs declared for hand-included JavaScript |
Ordered checks
- Reproduce locally with the release profile. Build with the same
:advancedsettings you deploy. If it works locally but fails in CI or on the server, compare compiler versions first — renaming behavior varies with the Closure version bundled in each ClojureScript release. - Read the exact failing symbol. Open the browser console and copy the full error, including the mangled name and stack trace.
- Rebuild with
:pseudo-names true. This keeps renames but makes them readable ($cljs$core$...$$-style names), so you can see which original property was renamed. Add it to your release build config temporarily — never ship it, since output is larger and slower. - Grep the emitted bundle for the original property name. If it appears only as a string and never as a property access, the call site was renamed.
- Classify the call site. Is it your own interop on a JS object, a call into an npm library, or pure ClojureScript data code? Pure ClojureScript code failing under
:advancedis rare and points elsewhere (e.g., a compiler bug or a macro emitting interop).
Fixes matched to findings
Your own interop on plain JS objects
Replace untyped property access with stable string-based access via goog.object, which Closure never renames:
(ns my.app
(:require [goog.object :as gobj]))
;; Instead of (.-userName js-obj)
(gobj/get js-obj "userName")
;; Nested access: instead of (.. js-obj -profile -email)
(gobj/getValueByKeys js-obj "profile" "email")
Alternatively, add ^js type hints so the analyzer knows the value is a JS object and preserves the property name (supported by shadow-cljs and recent ClojureScript compilers):
(defn greet [^js user]
(str "Hello " (.-name user)))
Reserve aget/aset for arrays. Using them for object property access is a common source of exactly this bug.
Third-party JavaScript
If you use shadow-cljs, it generates externs for node_modules automatically — check that your dependency actually comes from npm and not a manually included JS file. With lein/figwheel or raw cljs.main, you must supply externs yourself for non-npm JavaScript:
; build config
{:optimizations :advanced
:externs ["externs/mylib.js"]}
Keep externs minimal — only the properties you actually call. Wildcard externs suppress dead-code elimination and can mask real breakage.
Before rewriting access patterns
goog.object/get returns undefined for genuinely missing keys too. Confirm the property exists on the object (log it in the dev build) so you fix renaming, not a data-shape bug.
Escalation criteria
If a minimal reproduction — one namespace, one interop call — still fails with :pseudo-names enabled and correct externs in place, report it to the library maintainer or the ClojureScript issue tracker with the repro and your compiler versions (check project.clj, deps.edn, or shadow-cljs.edn). As an interim mitigation, wrap the fragile library behind a small precompiled JS module so Closure never sees its internals, or temporarily drop to :optimizations :simple, accepting a larger bundle until a proper fix lands.
Behavior around externs, ^js hints, and npm interop differs between toolchains and versions — verify against the tooling your project actually pins, and consult the official ClojureScript advanced-compilation documentation for your release.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.