#27705·emscripten

C++ exception in promise is freed prematurely, and destructor does not run

Author: stevenwdvCreated Sep 14, 2026Updated Sep 14, 2026

With Emscripten 6.0.9, a C++ exception escaping an asynchronous val Promise is freed before it reaches a JS caller. This happens in the val::promise logic in the ~val destructor of the error it rejects with (obtained via _emval_from_current_cxa_exception).

Moreover, even though the memory is freed, the destructor of the C++ exception never gets run.

Given mwe.cpp:

cpp
#include <emscripten/bind.h>
#include <emscripten/val.h>
#include <iostream>
#include <stdexcept>
#include <string>

using emscripten::val;

class MyError : public std::exception {
  volatile std::uint32_t check{0xdeadbeef};
public:
  MyError() { std::clog << "MyError" << std::endl; }
  ~MyError() { std::clog << "~MyError" << std::endl; check = 0; }
  const char *what() const noexcept override {
    return check == 0xdeadbeef ? "MyError (alive)" : "MyError (USE-AFTER-FREE!)";
  }
};

/// Coroutine that rejects its Promise with a C++ exception
val rejectAsync() {
  // Ensure a real suspension happens first, so the exception is thrown from a resumed coroutine
  co_await val::global("Promise").call<val>("resolve");
  throw MyError();
}

void throwSync() {
  throw MyError();
}

EMSCRIPTEN_BINDINGS(mwe) {
  emscripten::function("rejectAsync", &rejectAsync);
  emscripten::function("throwSync", &throwSync);
}

And test.mjs:

javascript
#!/usr/bin/env node
import initModule from './mwe.mjs';
const Module = await initModule();

function describe(label, ex) {
  console.log(`${label}: caught`, ex?.constructor?.name);
  try {
    console.log(`${label}: getExceptionMessage =`, Module.getExceptionMessage(ex));
    Module.decrementExceptionRefcount(ex);
    console.log(`${label}: decrementExceptionRefcount OK`);
  } catch (e) {
    console.log(`${label}: FAILED:`, e);
  }
}

try { Module.throwSync(); } catch (ex) { describe('sync', ex); }

try { await Module.rejectAsync(); } catch (ex) { describe('async', ex); }
bash
em++ mwe.cpp -o mwe.mjs -std=c++20 -lembind -fwasm-exceptions -sEXPORTED_RUNTIME_METHODS=getExceptionMessage,decrementExceptionRefcount -v
Compile log
 /home/swdv/emsdk/upstream/bin/clang --version
shared:INFO: (Emscripten: Running sanity checks)
 "/home/swdv/emsdk/upstream/bin/clang++" -target wasm32-unknown-emscripten -mllvm -combiner-global-alias-analysis=false -mllvm -wasm-enable-sjlj -mllvm -wasm-use-legacy-eh -mllvm -disable-lsr --sysroot=/home/swdv/emsdk/upstream/emscripten/cache/sysroot -Xclang -iwithsysroot/include/fakesdl -Xclang -iwithsysroot/include/compat -std=c++20 -fwasm-exceptions -v -c mwe.cpp -o /tmp/emscripten_temp_p7hbp8i3/mwe.o
clang version 24.0.0git (https:/github.com/llvm/llvm-project 510126255f89d693717c1ce7105b593f995f07c1)
Target: wasm32-unknown-emscripten
Thread model: posix
InstalledDir: /home/swdv/emsdk/upstream/bin
 (in-process)
 "/home/swdv/emsdk/upstream/bin/clang-24" -cc1 -triple wasm32-unknown-emscripten -emit-obj -disable-free -clear-ast-before-backend -disable-llvm-verifier -discard-value-names -main-file-name mwe.cpp -mrelocation-model static -mframe-pointer=none -ffp-contract=on -fno-rounding-math -mconstructor-aliases -target-feature +exception-handling -target-feature +multivalue -target-feature +reference-types -exception-model=wasm -mllvm -wasm-enable-eh -target-cpu generic -fvisibility=hidden -debugger-tuning=gdb -fdebug-compilation-dir=/tmp/claude-1000/-home-swdv-pep-core/3161dffd-f72e-47c9-a01e-f3118c6f3059/scratchpad/mwe -v -fcoverage-compilation-dir=/tmp/claude-1000/-home-swdv-pep-core/3161dffd-f72e-47c9-a01e-f3118c6f3059/scratchpad/mwe -resource-dir /home/swdv/emsdk/upstream/lib/clang/24 -isysroot /home/swdv/emsdk/upstream/emscripten/cache/sysroot -internal-isystem /home/swdv/emsdk/upstream/emscripten/cache/sysroot/include/wasm32-emscripten/eh/c++/v1 -internal-isystem /home/swdv/emsdk/upstream/emscripten/cache/sysroot/include/wasm32-emscripten/c++/v1 -internal-isystem /home/swdv/emsdk/upstream/emscripten/cache/sysroot/include/c++/v1 -internal-isystem /home/swdv/emsdk/upstream/lib/clang/24/include -internal-isystem /home/swdv/emsdk/upstream/emscripten/cache/sysroot/include/wasm32-emscripten -internal-isystem /home/swdv/emsdk/upstream/emscripten/cache/sysroot/include -std=c++20 -fdeprecated-macro -ferror-limit 19 -fmessage-length=382 -fgnuc-version=4.2.1 -fno-implicit-modules -fskip-odr-check-in-gmf -fcxx-exceptions -fexceptions -exception-model=wasm -iwithsysroot/include/fakesdl -iwithsysroot/include/compat -mllvm -combiner-global-alias-analysis=false -mllvm -wasm-enable-sjlj -mllvm -wasm-use-legacy-eh -mllvm -disable-lsr -o /tmp/emscripten_temp_p7hbp8i3/mwe.o -x c++ mwe.cpp
clang -cc1 version 24.0.0git based upon LLVM 24.0.0git default target x86_64-unknown-linux-gnu
ignoring nonexistent directory "/home/swdv/emsdk/upstream/emscripten/cache/sysroot/include/wasm32-emscripten/eh/c++/v1"
ignoring nonexistent directory "/home/swdv/emsdk/upstream/emscripten/cache/sysroot/include/wasm32-emscripten/c++/v1"
ignoring nonexistent directory "/home/swdv/emsdk/upstream/emscripten/cache/sysroot/include/wasm32-emscripten"
#include "..." search starts here:
#include <...> search starts here:
 /home/swdv/emsdk/upstream/emscripten/cache/sysroot/include/fakesdl
 /home/swdv/emsdk/upstream/emscripten/cache/sysroot/include/compat
 /home/swdv/emsdk/upstream/emscripten/cache/sysroot/include/c++/v1
 /home/swdv/emsdk/upstream/lib/clang/24/include
 /home/swdv/emsdk/upstream/emscripten/cache/sysroot/include
End of search list.
 /home/swdv/emsdk/node/24.19.0_64bit/bin/node /home/swdv/emsdk/upstream/emscripten/tools/compiler.mjs - --symbols-only
 /home/swdv/emsdk/upstream/bin/wasm-ld -o mwe.wasm /tmp/tmp72zg_dtnlibemscripten_js_symbols.so -Bstatic --strip-debug --export=emscripten_stack_get_end --export=emscripten_stack_get_free --export=emscripten_stack_get_base --export=emscripten_stack_get_current --export=emscripten_stack_init --export=_emscripten_stack_alloc --export=__getTypeName --export=__trap --export=__wasm_call_ctors --export=__get_exception_message --export=free --export=_emscripten_stack_restore --export=__thrown_object_from_unwind_exception --export=__cpp_exception --export=__cxa_decrement_exception_refcount --export-if-defined=__start_em_asm --export-if-defined=__stop_em_asm --export-if-defined=__start_em_lib_deps --export-if-defined=__stop_em_lib_deps --export-if-defined=__start_em_js --export-if-defined=__stop_em_js --export-if-defined=main --export-if-defined=__main_argc_argv --export-if-defined=fflush --export-table -z stack-size=65536 --no-growable-memory --initial-heap=16777216 --no-entry --stack-first --table-base=1 /tmp/emscripten_temp_p7hbp8i3/mwe.o -lembind-rtti -L/home/swdv/emsdk/upstream/emscripten/cache/sysroot/lib/wasm32-emscripten -L/home/swdv/emsdk/upstream/emscripten/src/lib -lGL-getprocaddr -lal -lhtml5 -lstubs-debug -lnoexit -lc-debug -ldlmalloc-debug -lclang_rt.builtins-legacysjlj -lc++-debug-legacyexcept -lc++abi-debug-legacyexcept -lunwind-legacyexcept -lsockets -mllvm -combiner-global-alias-analysis=false -mllvm -wasm-enable-sjlj -mllvm -wasm-use-legacy-eh -mllvm -disable-lsr -mllvm -wasm-enable-eh -mllvm -exception-model=wasm
 /home/swdv/emsdk/upstream/bin/llvm-objcopy mwe.wasm mwe.wasm '--remove-section=llvm.func_attr.annotate.*' --remove-section=producers --remove-section=name '--remove-section=.debug*'
 /home/swdv/emsdk/node/24.19.0_64bit/bin/node /home/swdv/emsdk/upstream/emscripten/tools/compiler.mjs -
 /home/swdv/emsdk/node/24.19.0_64bit/bin/node /home/swdv/emsdk/upstream/emscripten/tools/preprocessor.mjs - modularize.js --expand-macros

The rejectAsync case does not behave as expected:

$ ./test.mjs 
MyError
sync: caught Exception
sync: getExceptionMessage = [ 'MyError', 'MyError (alive)' ]
~MyError
sync: decrementExceptionRefcount OK
MyError
async: caught Exception
async: FAILED: RuntimeError: memory access out of bounds
    at wasm://wasm/0011e712:wasm-function[1692]:0x3fbc6
    at file:///tmp/claude-1000/-home-swdv-pep-core/3161dffd-f72e-47c9-a01e-f3118c6f3059/scratchpad/mwe/mwe.mjs:564:12
    at getExceptionMessageCommon (file:///tmp/claude-1000/-home-swdv-pep-core/3161dffd-f72e-47c9-a01e-f3118c6f3059/scratchpad/mwe/mwe.mjs:881:7)
    at Object.getExceptionMessage (file:///tmp/claude-1000/-home-swdv-pep-core/3161dffd-f72e-47c9-a01e-f3118c6f3059/scratchpad/mwe/mwe.mjs:896:14)
    at describe (file:///tmp/claude-1000/-home-swdv-pep-core/3161dffd-f72e-47c9-a01e-f3118c6f3059/scratchpad/mwe/test.mjs:9:59)
    at file:///tmp/claude-1000/-home-swdv-pep-core/3161dffd-f72e-47c9-a01e-f3118c6f3059/scratchpad/mwe/test.mjs:19:50

Source: emscripten-core/emscripten