Compiling C++ Exceptions for Wasm
This page answers one task: a C++ codebase uses try/throw/catch, and you need to compile it with Emscripten so exceptions actually work — choosing
between the available exception models with an eye on binary size, speed and the browsers you support.
Prerequisites
- [ ] Emscripten (emsdk) 3.1.50 or newer.
- [ ] A C++ project that throws exceptions — directly or through libraries such as the standard library’s
std::out_of_range. - [ ] The list of browsers and runtimes the build must support.
Three ways to handle exceptions
WebAssembly 1.0 had no exception instructions, so for years compilers had to emulate C++ exceptions. Emscripten offers three models, selected by flags.
With exceptions disabled (the historical default, -fignore-exceptions or no exception flags), throw aborts the program; this is the smallest and
fastest option and fine for code that never relies on catching. JavaScript-based exceptions (-fexceptions) implement try blocks by calling out
to JavaScript, which uses its own try/catch to unwind; it works in every browser but makes code that contains try blocks larger and slower, because
calls that might throw become calls through JavaScript. Native WebAssembly exception handling (-fwasm-exceptions) uses the exception-handling
proposal’s try/catch/throw instructions (now try_table and throw_ref in the final spec), executed by the engine with near-zero cost when nothing is
thrown.
Native exceptions are supported in all current major browsers and in Node 17+, so for most projects today they are the right default. The JavaScript-based model remains useful for runtimes without the proposal.
Step 1 — check whether you need exceptions at all
Search the code for catch blocks that do meaningful recovery. Many codebases throw but never catch except at the top level, in which case aborting with a
message is acceptable, and the disabled model saves size. Note that libraries may rely on exceptions internally — a JSON library that throws on parse
errors and catches internally to return an error code will misbehave with exceptions disabled. Build with exceptions disabled and run the test suite;
any test that expects an error but gets an abort reveals a dependency on catching.
Step 2 — build with native Wasm exceptions
Pass the flag to every compile and link step, including libraries:
emcc -O2 -fwasm-exceptions -c src/parser.cpp -o build/parser.o
emcc -O2 -fwasm-exceptions -c src/main.cpp -o build/main.o
emcc -O2 -fwasm-exceptions build/*.o -o dist/app.js -sMODULARIZE -sEXPORT_ES6
In CMake, add the flag to both CMAKE_CXX_FLAGS and CMAKE_EXE_LINKER_FLAGS, or use target_compile_options and target_link_options on every target.
Mixing objects compiled with different models produces link errors such as “undefined symbol: __cxa_find_matching_catch” or, worse, builds that compile
and then abort at the first throw. Emscripten ports and system libraries are rebuilt automatically for the selected model.
Step 3 — or use JavaScript-based exceptions for older runtimes
If you must support engines without the exception-handling proposal, build with -fexceptions instead, again on every compile and link step. To limit the
size and speed cost, you can restrict which functions may catch with -sEXCEPTION_CATCHING_ALLOWED=['_ZN6Parser5parseEv', …] (mangled names), leaving the
rest of the program on the cheap path. Shipping two builds — native for modern browsers, JavaScript-based for the rest — with feature detection is
possible but rarely worth it now.
Step 4 — catch C++ exceptions from JavaScript
An exception that escapes an exported function reaches JavaScript. With native exceptions it arrives as a WebAssembly.Exception; Emscripten can turn it
into a readable message if you build with -sEXPORT_EXCEPTION_HANDLING_HELPERS, which exports getExceptionMessage:
try {
Module._parse_document(ptr, len);
} catch (e) {
if (e instanceof WebAssembly.Exception) {
const [type, message] = Module.getExceptionMessage(e); // e.g. ["std::runtime_error", "unexpected token"]
Module.decrementExceptionRefcount(e); // free the C++ exception object
throw new Error(`${type}: ${message}`);
}
throw e;
}
A cleaner design catches exceptions inside a C++ wrapper and returns an error code or message, so JavaScript never sees C++ exception objects. The broader approach is in handling C++ exceptions from Wasm in JavaScript.
Step 5 — measure size and speed
Compare the three builds on your code. Typical results: disabling exceptions gives the smallest binary; native exceptions add a few percent; JavaScript-based
exceptions can add 10–30% to the size of code with many try blocks and noticeably slow calls inside them. Measure throw-heavy paths separately — parsers
that throw on every malformed input pay unwinding costs that differ between models.
Exceptions and RAII cleanup
C++ code relies on destructors running during unwinding: locks released, memory freed, files closed. With exceptions disabled, a throw aborts and no
destructors run — acceptable when the abort ends the program, but in a long-lived WebAssembly instance that keeps serving calls after a caught JavaScript
error, it can leak memory or leave state inconsistent. With both exception models enabled, destructors run as expected during unwinding. That is one of the
strongest reasons to enable exceptions in libraries that are used interactively: an error in one call should not poison the instance for the next. If you
keep exceptions disabled, treat any abort as fatal for the instance and re-create it, as described in
recovering a module after a trap.
Debugging exception problems
When exceptions misbehave, three checks find most problems. Confirm that every object and library was built with the same model: llvm-nm on objects
shows references to __cxa_* helpers whose names differ between models, and wasm-tools print on the final module shows whether try_table or
invoke_* imports are present. Build with -sASSERTIONS to get readable messages for uncaught exceptions instead of a bare abort. And check the runtime’s
support: Node versions before 17 and some embedded runtimes lack the proposal, producing a CompileError mentioning an unknown opcode, which is the cue
to fall back to JavaScript-based exceptions for that target.
Exceptions in mixed C++ and Rust or JavaScript stacks
Real applications rarely consist of C++ alone. When C++ code compiled with Emscripten calls back into JavaScript through imports, and that JavaScript
throws, the JavaScript exception unwinds through the Wasm frames. With native exception handling, C++ catch (...) blocks can intercept such foreign
exceptions, which may run destructors and then rethrow; with JavaScript-based exceptions the behaviour depends on whether the call went through an
invoke wrapper. The predictable design is to never let exceptions cross the language boundary in either direction: catch JavaScript errors inside the
import implementation and return an error code to C++, and catch C++ exceptions in the exported wrapper and return an error to JavaScript. Linking C++
with Rust in one module adds another layer, since Rust panics and C++ exceptions are different mechanisms; keep each language’s errors inside its own code
and convert at the boundary between them, as with JavaScript.
Expected output
The native-exception build catches std::runtime_error from the parser and returns an error message to JavaScript; the binary is 3% larger than with
exceptions disabled; the normal path is as fast as the disabled build; and every object in the build references the native-EH helpers only.
Gotchas
- Mixing exception models across objects. Link errors or aborts at the first throw. Use one flag everywhere.
- Leaking exception objects in JavaScript. Call
decrementExceptionRefcountafter inspecting them. - Disabling exceptions in libraries that catch internally. They abort instead of returning errors. Test error paths.
- Assuming old runtimes support native EH. Check targets; fall back to
-fexceptionsif needed. - Throwing across exports. Prefer catching in C++ and returning codes.
Performance note
For a parser with many try blocks, the release binary was 412 KB with exceptions disabled, 425 KB with native exceptions and 528 KB with JavaScript-based
exceptions. Parsing valid input took 18 ms, 18 ms and 26 ms respectively in Chrome.
Frequently Asked Questions
Is native Wasm exception handling final?
The proposal is standardised; newer toolchains emit the final try_table form, supported in current engines.
Does Rust use these models?
Rust panics abort on wasm32-unknown-unknown by default; unwinding with Wasm EH is available on nightly with build-std.
Can I catch JavaScript exceptions in C++? JavaScript exceptions thrown through imports can be caught as foreign exceptions with native EH; most code converts them to error codes in the import.
Do exceptions affect -O3 optimisation?
Native exceptions have little effect on optimisation; JavaScript-based exceptions inhibit some inlining around invoke calls.
What about setjmp/longjmp?
Emscripten supports them with -sSUPPORT_LONGJMP, using Wasm EH or JavaScript, matching the exception model.
Should exceptions ever cross from C++ to JavaScript? It works, but converting them to error codes or messages in a C++ wrapper keeps the boundary simpler and avoids leaked exception objects.
Do exceptions need the same flag at compile and link time? Yes — mixing exception modes between objects causes link errors or undefined behaviour.
Related
- Exception handling in WebAssembly — the proposal itself.
- Debugging Emscripten builds with assertions — readable aborts.
- Mapping C error codes to JavaScript errors — the error-code alternative.
- Building Emscripten projects with CMake — setting flags consistently.
← Back to C/C++ to Wasm with Emscripten