Handling C++ Exceptions from Wasm in JavaScript
This page answers one task: a C++ library compiled with Emscripten throws exceptions — std::runtime_error, std::invalid_argument, custom types —
and when one escapes to JavaScript you get an opaque object or a number instead of a useful error. You want to catch these exceptions in JavaScript,
read their type and message, and turn them into proper JavaScript errors.
Prerequisites
- [ ] A C++ codebase built with Emscripten, with exceptions enabled.
- [ ] A recent Emscripten (3.1.x or later).
- [ ] A JavaScript wrapper around the exported functions.
Two ways C++ exceptions work in Wasm
Emscripten supports two implementations. The older one, enabled with -fexceptions, implements throw and catch with JavaScript: each try region
becomes calls through JavaScript invoke_* helpers, and a thrown exception is a JavaScript value carrying a pointer to the C++ exception object in linear
memory. It works everywhere but adds code size and slows every call inside a try region.
The newer one, enabled with -fwasm-exceptions, uses WebAssembly’s native exception-handling instructions (try/catch or the newer try_table and
throw_ref). A C++ exception becomes a WebAssembly.Exception with a tag identifying it as a C++ exception and an argument holding the pointer to the
exception object. It is faster and smaller, and supported in all current major browsers. Either way, when the exception reaches JavaScript, what you hold
is a handle to a C++ object living in Wasm memory — not a JavaScript error with a message. Converting it is the wrapper’s job.
Step 1 — build with native exceptions and the helpers
emcc src/*.cpp -O2 -fwasm-exceptions \
-sEXPORT_EXCEPTION_HANDLING_HELPERS \
-sEXPORTED_RUNTIME_METHODS=getExceptionMessage,incrementExceptionRefCount,decrementExceptionRefCount \
-o lib.js
EXPORT_EXCEPTION_HANDLING_HELPERS makes getExceptionMessage and the reference-count helpers available on the module object. Compile all C++ code
with the same flag — mixing -fexceptions and -fwasm-exceptions objects in one link fails.
Step 2 — catch and identify the exception
import createModule from "./lib.js";
const Module = await createModule();
try {
Module._parse_config(ptr, len);
} catch (e) {
if (e instanceof WebAssembly.Exception) {
const [type, message] = Module.getExceptionMessage(e);
console.error(`C++ ${type}: ${message}`); // "std::invalid_argument: missing key 'name'"
} else {
throw e; // a trap or a JavaScript error
}
}
getExceptionMessage returns the demangled type name and, for exceptions derived from std::exception, the result of what(). It works with both
exception modes (with -fexceptions the caught value is not a WebAssembly.Exception, but the helper accepts it). Distinguish exceptions from traps:
a WebAssembly.RuntimeError is a trap, not a C++ exception, and needs different handling.
Step 3 — translate at the wrapper boundary
Do not let WebAssembly.Exception objects escape into application code. Translate them into JavaScript errors with stable codes in one place:
const CPP_TO_CODE = {
"std::invalid_argument": "INVALID_ARGUMENT",
"std::out_of_range": "OUT_OF_RANGE",
"config::ParseError": "PARSE_ERROR",
};
function callCpp(fn, ...args) {
try {
return fn(...args);
} catch (e) {
if (!(e instanceof WebAssembly.Exception)) throw e;
const [type, message] = Module.getExceptionMessage(e);
Module.decrementExceptionRefCount(e); // release the C++ object
const err = new Error(message || type);
err.name = "CppError";
err.code = CPP_TO_CODE[type] ?? "CPP_EXCEPTION";
err.cppType = type;
throw err;
}
}
export const parseConfig = (text) => withString(text, (ptr, len) => callCpp(Module._parse_config, ptr, len));
Step 4 — avoid leaking exception objects
The C++ exception object lives in linear memory and is reference-counted. When it propagates out to JavaScript and is not rethrown into C++, nothing
frees it unless you do. Call decrementExceptionRefCount(e) after extracting what you need, as above. If you need to keep the exception around — to
rethrow it into C++ later — increment the count first. Leaking one small object per failed call is invisible in tests and adds up in long sessions;
check with a counting allocator or mallinfo in a test that triggers the exception repeatedly.
Step 5 — prefer catching inside C++ for expected errors
Exceptions crossing the boundary are fine for genuinely exceptional conditions. For expected failures — invalid user input, missing files — it is
simpler to catch in a thin C++ wrapper and return a status plus message, so JavaScript never sees a WebAssembly.Exception:
#include <emscripten/bind.h>
struct Result { bool ok; std::string error; Config value; };
Result parse_config_safe(const std::string& text) {
try { return { true, "", parse_config(text) }; }
catch (const std::exception& e) { return { false, e.what(), {} }; }
}
EMSCRIPTEN_BINDINGS(cfg) {
emscripten::value_object<Result>("Result")
.field("ok", &Result::ok).field("error", &Result::error).field("value", &Result::value);
emscripten::function("parseConfig", &parse_config_safe);
}
Embind can also be configured to translate exceptions, but an explicit result type makes the error contract visible in the API.
Throwing JavaScript errors into C++
The reverse direction also happens: an imported JavaScript function throws while C++ code is on the stack. With native exception handling, a JavaScript
exception propagates through Wasm frames as a foreign exception; C++ catch (...) can catch it in recent toolchains, but typed catches will not match,
and destructors run during unwinding. The safest design is to catch JavaScript errors inside the imported function and return an error status to C++, so
C++ code never unwinds through a JavaScript exception unexpectedly.
Performance considerations
With -fexceptions, every call inside a try region goes through an invoke_* JavaScript helper, which can make exception-heavy code several times
slower even when nothing is thrown. Native Wasm exceptions have near-zero cost when not thrown, and throwing is reasonably fast. If profiles show
invoke_ functions prominently, switching to -fwasm-exceptions is usually the largest available speed-up for that code.
Inspecting exceptions without the Emscripten helpers
Code that loads a module without Emscripten’s JavaScript runtime — a custom loader, or a module built with plain clang and wasi-sdk — cannot call
getExceptionMessage. The underlying mechanism is still accessible. A WebAssembly.Exception carries a tag; the module exports its C++ exception tag
(commonly as __cpp_exception), and exception.is(tag) tells you whether a caught value is a C++ exception. exception.getArg(tag, 0) returns the
pointer to the thrown object in linear memory. From there, the module itself is the best place to interpret it: export a small C++ function that takes
that pointer, casts it to std::exception* inside a try/catch (by rethrowing and catching, which handles arbitrary types safely), and writes the
type name and what() into a buffer JavaScript can read. That is essentially what Emscripten’s helper does, and writing it yourself keeps the custom
loader independent of the Emscripten runtime.
Testing exception paths
Exceptions that cross the boundary deserve tests of their own, because they involve three cooperating parts: the C++ throw, the engine’s propagation and the wrapper’s translation. For each exception type the API documents, write a JavaScript test that triggers it and asserts the translated error’s code, type and message. Add a test that triggers an exception thousands of times and asserts that heap size stays flat, which catches missing reference-count releases. Run the tests in every browser you support, since native exception handling is a relatively recent engine feature and engine bugs, while rare, have occurred around edge cases such as exceptions thrown during stack overflow.
Choosing an error contract for new code
For new C++ code designed to be called from JavaScript, decide the error contract up front. Exceptions inside C++ for internal control flow are fine;
the exported surface should have one documented behaviour — either “returns a result object” or “throws a translated CppError with these codes” — and
never let raw WebAssembly.Exception values reach callers.
Expected output
Calling parseConfig("{}") from JavaScript throws CppError: missing key 'name' with code: "INVALID_ARGUMENT" and cppType: "std::invalid_argument";
10,000 failing calls leave the heap size unchanged; traps still surface as WebAssembly.RuntimeError; and the build is 14% smaller than the
-fexceptions version.
Gotchas
- Mixing exception flags across objects. The link fails or behaves inconsistently. Use one mode everywhere.
- Not exporting the helpers.
getExceptionMessageis undefined. AddEXPORT_EXCEPTION_HANDLING_HELPERS. - Leaking exception objects. Decrement the reference count after handling.
- Treating traps as exceptions. A
RuntimeErroris a trap, not a C++ exception. - Exceptions for expected errors. Return a result type instead.
- Untested exception paths. Translation and reference counting break silently. Test each documented exception type, repeatedly.
Performance note
For a parser with many try regions, switching from -fexceptions to -fwasm-exceptions cut parse time from 48 ms to 19 ms when no exceptions were
thrown, and reduced the module from 1.31 MB to 1.13 MB.
Frequently Asked Questions
Do all browsers support native Wasm exceptions?
All current major browsers do; old versions need -fexceptions.
Can I get a C++ stack trace?
Build with -sASSERTIONS and debug info during development; the JavaScript stack includes Wasm frames.
Does Embind translate exceptions automatically? Not by default; catch in C++ or translate in the wrapper.
What about -sDISABLE_EXCEPTION_CATCHING?
It disables catching in the JavaScript-based mode; throws then abort. It does not apply to native exceptions.
Can I identify C++ exceptions without Emscripten’s runtime?
Yes — check exception.is(tag) against the module’s exported C++ exception tag and read the object pointer with getArg.
Related
- Compiling C++ exceptions for Wasm — the build side.
- Exception handling in WebAssembly — the proposal.
- Mapping C error codes to JavaScript errors — the result-code alternative.
- Adding context to errors from Wasm — what to attach.
← Back to Errors & Traps Across the Boundary