Skip to content

Since v4.0, module instantiation errors can’t be caught because two errors are thrown if MODULARIZE=1 #24415

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
FelixNumworks opened this issue May 27, 2025 · 9 comments · Fixed by #24418

Comments

@FelixNumworks
Copy link

FelixNumworks commented May 27, 2025

Version of emscripten/emsdk: 4.0.8 (bug introduced in 4.0.0)

Description

I'm compiling with the MODULARIZE=1 flag and using code like the following to instantiate the module in js:

try {
  module = await Module({
    locateFile,
    fetchSettings,
  });
} catch (e) {
  console.warn(e);
  // redirect to error page
}

Before version 4.0, everything worked as expected: if the .wasm file failed to load, the error was caught and handled.

Since version 4.0, however, when the .wasm fetch fails, two errors are thrown in sequence. I can only catch one, but another one is thrown later and can't be caught because the catch block has already run.

Cause

This issue appears to originate from the following line in Module.js:

var wasmExports = await createWasm();

This line is generated by this one:

emscripten/tools/emscripten.py

Lines 1047 to 1052 in fe95793

if can_use_await():
# In modularize mode the generated code is within a factory function.
# This magic string gets replaced by `await createWasm`. It needed to allow
# closure and acorn to process the module without seeing this as a top-level
# await.
module.append("var wasmExports = EMSCRIPTEN$AWAIT(createWasm());\n")

This was introduced by this PR #23157

Here’s a minimal reproduction of the Module.js structure:

// Inside Module.js
function createWasm() {
  try {
    // code that throws error1
  } catch (error1) {
    readyPromiseReject(error1);
    var error2 = error1; // I'm adding this line to differentiate between the two
    return Promise.reject(error2); // <- error2 is caught, error1 is not
  }
}

var wasmExports = await createWasm();
// In my application code
try {
  await Module();
} catch (e) {
  console.warn(e); // Only error2 is caught here
}
// error1 is later thrown unhandled

So error2 is caught by user code, but error1 is thrown after the fact and cannot be intercepted, leading to an unhandled rejection or uncaught error.

Suggested Fix

Modifying the Module.js file this way fixes the problem:

try {
  var wasmExports = await createWasm();
} catch (err) {
  readyPromiseReject(err);
  return readyPromise;
}

This avoids throwing a second error, but it’s obviously not ideal.

The core issue is mixing a top-level await with a promise (readyPromise) that is also being rejected separately. This leads to two separate error flows. In my opinion, the code should use either top-level await or a returned/rejected promise—not both.

@FelixNumworks
Copy link
Author

@sbc100 you’ve worked on this code recently—do you have any thoughts on this?
Am I the one doing something wrong ?

@sbc100
Copy link
Collaborator

sbc100 commented May 27, 2025

Thanks for debugging the issue @FelixNumworks. I certainly believe there is a fix like this needed here.

(By the way you say "top-level await" but IIUC that await you are refering to is the one inside the Module factory function right? So its not top level in the actual JS sense, right?. Also, BTW, it would be an actual top level await if you used the new/experimental -sMODULARIZE=instance setting: https://emscripten.org/docs/compiling/Modularized-Output.html)

@FelixNumworks
Copy link
Author

By the way you say "top-level await" but IIUC that await you are refering to is the one inside the Module factory function right? So its not top level in the actual JS sense, right?

Yes I meant top-level in the factory function, not in the actual sense, sorry ^^"

This MODULARIZE=instance is very instersting, I'll look a bit into it, ty :)

sbc100 added a commit to sbc100/emscripten that referenced this issue May 27, 2025
In `MODUALRIZE` mode we always do `await createWasm` so means any
uncaught exceptions in `createWasm` will be thrown there, before
`readyPromise` is event returned from the factory function.

Fixes: emscripten-core#24415
@sbc100
Copy link
Collaborator

sbc100 commented May 27, 2025

I have a fix in flight: #24418. I'm still working on test for it.

sbc100 added a commit to sbc100/emscripten that referenced this issue May 27, 2025
In `MODUALRIZE` mode we always do `await createWasm` so means any
uncaught exceptions in `createWasm` will be thrown there, before
`readyPromise` is event returned from the factory function.

Fixes: emscripten-core#24415
sbc100 added a commit to sbc100/emscripten that referenced this issue May 27, 2025
In `MODUALRIZE` mode we always do `await createWasm` so means any
uncaught exceptions in `createWasm` will be thrown there, before
`readyPromise` is event returned from the factory function.

Fixes: emscripten-core#24415
sbc100 added a commit to sbc100/emscripten that referenced this issue May 27, 2025
In `MODUALRIZE` mode we always do `await createWasm` so means any
uncaught exceptions in `createWasm` will be thrown there, before
`readyPromise` is event returned from the factory function.

Fixes: emscripten-core#24415
@sbc100
Copy link
Collaborator

sbc100 commented May 27, 2025

If are you able could you confirm whether #24418 fixes the issue?

@FelixNumworks
Copy link
Author

I tested it, it works perfectly :)
Thanks a lot for your reactivity !!!

@FelixNumworks
Copy link
Author

FelixNumworks commented May 28, 2025

Just as a side note, since we discussed this subject earlier, -sMODULARIZE=instance isn't suitable for my use case. It doesn't seem to work well with EMSCRIPTEN_BINDINGS from embind, since all the bindings are tied to the Module object, which isn’t exported or accessible when using -sMODULARIZE=instance.

@sbc100
Copy link
Collaborator

sbc100 commented May 28, 2025

The goal is certainly to make embind work with -sMODULARIZE=instance. You should be able to import the bindings directly I think. We run embind in AOT mode in this case. If you are having issue using it you file an issue and @brendandahl can take a look.

sbc100 added a commit that referenced this issue May 28, 2025
In `MODUALRIZE` mode we always do `await createWasm` before we return
the readyPromise.

This means there is no need to call `readyPromiseReject` inside of
createWasm.

Furthermore, we can completely avoid creating the ready promise if we
don't need it and make all the resolve/reject sites call the
resolve/reject handlers conditionally.

Fixes: #24415
@brendandahl
Copy link
Collaborator

I added support fairly recently for embind and instance mode, let me know if you run into anything.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants