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.

Emscripten's exception models compared Disabled exceptions abort on throw and are smallest and fastest. JavaScript-based exceptions work everywhere but enlarge and slow code with try blocks. Native Wasm exceptions use engine support with almost no cost until a throw, and need a browser or runtime with the exception-handling proposal. model flag cost support disabled -fignore-exceptions none (throw aborts) everywhere JavaScript-based -fexceptions larger + slower try code everywhere native Wasm EH -fwasm-exceptions near zero until throw current engines

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.

Native versus JavaScript-based exceptions Native Wasm exceptions add almost no cost on the normal path and unwind inside the engine when thrown. JavaScript-based exceptions route potentially throwing calls through JavaScript helpers, increasing code size and slowing the normal path, but work in every engine. native (-fwasm-exceptions) zero-cost normal path unwinding inside the engine needs EH proposal support default today JS-based (-fexceptions) invoke_* helpers through JS bigger, slower try code works in any engine legacy runtimes

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 decrementExceptionRefcount after 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 -fexceptions if 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.

Binary size by exception model Kilobytes of release Wasm output for the same C++ parser built with exceptions disabled, with native Wasm exception handling, and with JavaScript-based exceptions. KB of .wasm exceptions disabled 412 KB native Wasm EH 425 KB JavaScript-based 528 KB

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.

← Back to C/C++ to Wasm with Emscripten