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.

JavaScript-based versus native Wasm exception handling With -fexceptions, try regions call through JavaScript invoke helpers and thrown exceptions are JavaScript values holding a pointer, which works everywhere but costs size and speed. With -fwasm-exceptions, the engine's native exception instructions handle throw and catch, and exceptions reaching JavaScript are WebAssembly.Exception objects holding the pointer. -fexceptions (JS-based) invoke_* helpers in JS larger and slower works in old engines legacy -fwasm-exceptions (native) engine try/catch/throw smaller and faster current browsers recommended

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));
A C++ exception reaching JavaScript C++ code throws an exception object allocated in linear memory. It propagates as a WebAssembly.Exception through the export to JavaScript. The wrapper catches it, reads the type and message with getExceptionMessage, releases the C++ object, and throws a JavaScript error with a stable code. throw in C++ object in linear memory Wasm exception crosses the export wrapper catches reads type + message release object drop the ref count JS Error with code application handles it

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. getExceptionMessage is undefined. Add EXPORT_EXCEPTION_HANDLING_HELPERS.
  • Leaking exception objects. Decrement the reference count after handling.
  • Treating traps as exceptions. A RuntimeError is 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.

Parse time with JavaScript-based and native exceptions Milliseconds to parse a large configuration with no exceptions thrown, built with JavaScript-based exception handling and with native Wasm exception handling. ms per parse -fexceptions 48 ms -fwasm-exceptions 19 ms

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.

← Back to Errors & Traps Across the Boundary