Troubleshooting — common errors and which override to reach for
When a cpp.js build or runtime fails, don't bypass the system — almost every common error has a designated override mechanism. This doc lists the errors agents actually hit and routes each to the right override (catalogued in
overrides.md).
The recovery rule:
- Read the error literally.
- Identify which layer it lives in (binding-time / build-time / link-time / runtime / hosting).
- Reach for the least invasive override (target filter → targetSpecs → env → build hooks → extensions).
- Don't manually edit cpp.js source, generated artifacts, or upstream library code outside
replaceList.
Build-time errors
Error: ENOENT: no such file or directory ... /dist/...
cpp.js expected a dist/ from a dependency that hasn't been built yet.
- Cause: transitive dep wasn't built. pnpm topological order should handle this, but if you ran a single-package build with
pnpm --filter <one>it skipped the deps. - Fix:
pnpm install && pnpm run build(full topological), orpnpm --filter '@cpp.js/package-<dep>*' run buildfirst.
undefined symbol: <name> at link
The linker can't find a symbol your code references.
- Cause A — missing dep:
dependencies: []incppjs.config.jsdoesn't list the package providing this symbol. Fix: add the dep + its sub-arch dep inpackage.json. See ADR-0002. - Cause B — symbol clash: two libs export the same symbol. Common with
iconv(gdal renames it tolibiconvto avoid clash). Fix:cppjs.build.jsreplaceListto rename one set of symbols, ortargetSpecs[].specs.ignoreLibNameto suppress the duplicate.
error: cannot find -l<libname>
Linker can't find a library file, but the dep IS declared.
- Cause: library was built but produced a different
.aname than expected. Usually a mismatch betweenexport.libNamein the dep and what's actually built. - Fix: check
cppjs-package-<dep>/cppjs-package-<dep>-<arch>/cppjs.config.jsexport.libNamearray; verify the produced.aexists indist/lib/.
wasm-ld: error: --whole-archive ... duplicate symbol
Two libs define the same function.
- Fix: same as "symbol clash" above.
replaceListrename, orignoreLibNameto drop one.
__wasm__ / CPL_CPUID / __asm__ / SIMD intrinsic compile errors
Upstream library uses CPU-specific code that doesn't compile for Wasm.
-
Fix:
cppjs.build.jsreplaceListto gate the intrinsic with#ifdef __wasm__. Real example: gdal-wasm.replaceList: [{regex: /CPL_CPUID\(1, cpuinfo\);/g,replacement: '#ifdef __wasm__\ncpuinfo[0]=0;\n#else\nCPL_CPUID(1, cpuinfo);\n#endif',paths: ['port/cpl_cpu_features.cpp'],}]
configure: error: cannot find ... (autotools project)
buildType: 'configure' but the configure script can't find a dep header / lib.
- Fix A: add the missing dep to
dependencies. Re-derivedepPaths.X.headeringetBuildParams. - Fix B: if your dep IS there,
getBuildParamsmay need explicit--with-X=...=${depPaths.X.header}flags. Verify the dep name indepPathsmatches the package short name.
RuntimeError: index out of bounds during build
Emscripten itself ran out of memory during compilation.
- Cause: linker working set exceeded default heap.
- Fix: rare; usually fixed in newer Emscripten. If hitting it:
targetSpecs[].specs.emccFlags: ['-sINITIAL_MEMORY=512MB'].
iOS: pipe2 was misdetected as available
curl-ios SDK 26+ specific.
- Cause: autoconf cache miss between SDK detection and actual symbol availability.
- Fix:
getBuildParamsadds'-D_CURL_PREFILL=ON'. See curl-ios's cppjs.build.js for the prefill cache pattern.
iOS: code signing fails on a build utility
Upstream lib has a tool / utility that's built and signed, but you don't have a dev cert.
- Fix:
targetSpecs[].specs.cmake: ['-DCMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED=NO']. Real example: jpegturbo-ios for SIMD coverage tool.
Binding-time errors
binding ... silently produces null or function returns undefined
Your C++ function violated a binding rule. See cpp-binding-rules.md.
- Returning
unique_ptrinstead ofshared_ptr? Switch. - Returning a raw pointer (
MyClass*)? Wrap inshared_ptror write a C++ wrapper. - Using multiple inheritance? Refactor to composition.
- Template not explicitly instantiated? Add
template class X<int>;. - Definition only in
.cpp, not in.h? Move definition (or at least public surface) into the header.
SWIG generation failures (%feature errors, Unable to find ...)
Auto-generated .i doesn't fit. Write a manual one — see swig-escape.md.
Error: Tried to call ... but the function is not exposed
Function not in the public binding surface.
- Cause A: function is in an anonymous namespace or
staticat file scope. Move to public surface. - Cause B: declared in
.hbut defined in.cppwith astaticorinlinequalifier the binder skips. Match declaration and definition.
Runtime errors
Browser console: crossOriginIsolated is false
You set target.runtime: 'mt' but production hosting isn't sending COOP/COEP headers.
- Fix: add
Cross-Origin-Opener-Policy: same-origin+Cross-Origin-Embedder-Policy: require-corpto your host config. Seethreading.mdper-host table (Vercel, Netlify, nginx, Cloudflare).
RuntimeError: SharedArrayBuffer is not defined
Same root cause as above — COOP/COEP missing.
OPFS is only available inside a Worker scope
You mounted /opfs/... but didn't set useWorker: true on initCppJs(opts).
- Fix:
initCppJs({ useWorker: true }). Seefilesystem.md.
Path /opfs/... but OPFS is disabled. Enable fs.opfs in config
You explicitly set fs: { opfs: false } and then tried to use /opfs/....
- Fix: either remove the
opfs: false(it's true by default) or mount under/memfs/....
RuntimeError: out of memory (Wasm runtime)
Wasm process exhausted its allocated heap.
- Note:
-sALLOW_MEMORY_GROWTH=1is on by default — runtime should grow memory automatically up to host limits. - Fix A — initial allocation:
targetSpecs[].specs.emccFlags: ['-sINITIAL_MEMORY=128MB']. Bump default starting size. - Fix B — maximum allocation:
targetSpecs[].specs.emccFlags: ['-sMAXIMUM_MEMORY=4GB']. Browser cap is ~4GB on wasm32; use wasm64 target if you need more. - Fix C — algorithmic: stream the input instead of loading all into memory. Split the workload. Most "out of memory" cases mean your design holds too much state at once.
Function is not a function / m.someFunc is undefined
The C++ function exists but didn't bind to JS.
- Cause: binding rule violation. See "Binding-time errors" above.
- Cause: the JS module finished loading but
initCppJshasn't completed. Make sure you're callingm.someFuncafterawait initCppJs(...).
Calls hang forever when useWorker: true
Comlink call posted to worker but no response.
- Cause A: worker script URL is wrong; worker never spawned. Check Network tab — should see a request to the worker
.js. - Cause B: C++ inside worker is in an infinite loop. Open Sources tab on the worker context.
Cross-cutting symptoms
wasm streaming compile failed
- Cause A: wrong MIME type. Some hosts serve
.wasmasapplication/octet-streaminstead ofapplication/wasm. Fix: host config to set the right MIME. - Cause B: mixed
mtandstartifacts in the same bundle. The runtime tries to load amt.wasmwith anstJS loader (or vice versa). Fix: clean rebuild after changingruntime.pnpm clear:cache && pnpm clear:distthen rebuild.
Build "works" but produces no output
- Cause:
functions.isEnabled: () => falseaccidentally returned false for everything. - Cause:
target.platformfilter is set to a value that doesn't match any built-in target. - Fix: check the build log for
Skipping target: ...lines.
Hot Module Reload doesn't pick up .cpp changes (Vite/Webpack)
- Cause:
paths.nativechange not detected by the bundler's file watcher. - Note: the bundler plugins (
@cpp.js/plugin-vite,-webpack) explicitly add thepaths.nativefiles to the watcher in dev mode. If HMR fails: - Fix: restart dev server. If it persists, check that
paths.nativeis correctly resolved (look atstate.config.paths.nativein console).
Tribal-knowledge gotchas (from real packages)
These are patterns the source code uses but aren't obvious. Flag them to users early:
1. iconv symbol collision in gdal
When you depend on both gdal and a transitive lib that uses libiconv, symbols clash. gdal uses a replaceList entry to prefix iconv calls. If you write a new package that also uses iconv, mirror the pattern.
2. emscripten_fetch swap in curl-wasm
curl's POSIX socket calls don't exist in Wasm. curl-wasm injects a 200-line replaceList swap to use Emscripten's fetch API. If you ship any HTTP-using package for Wasm, you'll need the same swap (or use curl as a dep).
3. CPU intrinsic gates
CPL_CPUID, __asm__, <immintrin.h> etc. don't compile for Wasm by default. gdal-wasm gates them with #ifdef __wasm__. Pattern is in gdal's cppjs.build.js.
4. depPaths silent fallback
When getBuildParams references depPaths.X.header with a wrong key name (typo or rename), CMake silently falls back to the system header search path. The build "succeeds" but uses the wrong header. Always verify the dep name in depPaths matches the package short name (the general.name of the dep).
5. iOS _CURL_PREFILL cache
On iOS SDK 26+, autoconf misdetects pipe2 due to a cache mismatch. curl-ios uses _CURL_PREFILL=ON to force-load a prefill script. Any autotools-based package on iOS may hit similar cache issues; check curl-ios's cppjs.build.js for the pattern.
Diagnostic flow
When the error doesn't match anything above:
pnpm run doctor— verifies Node, pnpm, Docker, Android SDK/NDK, Xcode. Most "weird" build failures are missing toolchains.- Check
~/.cppjs.jsonLOG_LEVEL: 'DEBUG'— turns on verbose tracing in cpp.js itself. Often shows which step failed. - Reduce to smallest reproducer — create a fresh project with just the failing dep. Apply the bug-fix playbook (
docs/playbooks/bug-fix.md). - Search for the error literal in
cppjs-core/cpp.js/src/. Most cpp.js error messages are unique enough to find the throwing site. - File an issue if the error originates from cpp.js itself, not from your config or upstream.
See also
overrides.md— the catalog of override mechanisms each fix uses.build-state.md—state/targetshapes forreplaceList/getBuildParams.cpp-binding-rules.md— binding-time error root causes.threading.md— COOP/COEP and edge-runtime limits.filesystem.md— OPFS / memfs runtime errors.docs/playbooks/bug-fix.md— the fix-a-bug workflow.