Skip to content

Propagate errors through val coroutine hierarchy. #23653

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

Merged
merged 12 commits into from
Jun 3, 2025
Merged
40 changes: 20 additions & 20 deletions src/lib/libemval.js
Original file line number Diff line number Diff line change
Expand Up @@ -456,33 +456,33 @@ var LibraryEmVal = {
return result.done ? 0 : Emval.toHandle(result.value);
},

_emval_coro_suspend__deps: ['$Emval', '_emval_coro_resume'],
_emval_coro_suspend: async (promiseHandle, awaiterPtr) => {
var result = await Emval.toValue(promiseHandle);
__emval_coro_resume(awaiterPtr, Emval.toHandle(result));
_emval_coro_suspend__deps: ['$Emval', '_emval_coro_resume', '_emval_coro_reject'],
_emval_coro_suspend: (promiseHandle, awaiterPtr) => {
Emval.toValue(promiseHandle)
.then((result) => __emval_coro_resume(awaiterPtr, Emval.toHandle(result)),
(error) => __emval_coro_reject(awaiterPtr, Emval.toHandle(error)));
},

_emval_coro_make_promise__deps: ['$Emval', '__cxa_rethrow'],
_emval_coro_make_promise__deps: ['$Emval'],
_emval_coro_make_promise: (resolveHandlePtr, rejectHandlePtr) => {
return Emval.toHandle(new Promise((resolve, reject) => {
const rejectWithCurrentException = () => {
try {
// Use __cxa_rethrow which already has mechanism for generating
// user-friendly error message and stacktrace from C++ exception
// if EXCEPTION_STACK_TRACES is enabled and numeric exception
// with metadata optimised out otherwise.
___cxa_rethrow();
} catch (e) {
// But catch it so that it rejects the promise instead of throwing
// in an unpredictable place during async execution.
reject(e);
}
};

{{{ makeSetValue('resolveHandlePtr', '0', 'Emval.toHandle(resolve)', '*') }}};
{{{ makeSetValue('rejectHandlePtr', '0', 'Emval.toHandle(rejectWithCurrentException)', '*') }}};
{{{ makeSetValue('rejectHandlePtr', '0', 'Emval.toHandle(reject)', '*') }}};
}));
},

_emval_from_current_cxa_exception__deps: ['$Emval', '__cxa_rethrow'],
_emval_from_current_cxa_exception: () => {
try {
// Use __cxa_rethrow which already has mechanism for generating
// user-friendly error message and stacktrace from C++ exception
// if EXCEPTION_STACK_TRACES is enabled and numeric exception
// with metadata optimised out otherwise.
___cxa_rethrow();
} catch (e) {
return Emval.toHandle(e);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aheejin does this look reasonable to you? Or is the maybe a better way to do this?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we wait for @aheejin's review or merge as-is? This piece of code was before this PR (I added it a while back because it was the only way to make it work that I found at the time), so I'm leaning towards let's merge it anyway.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ping @aheejin

},
};

addToLibrary(LibraryEmVal);
1 change: 1 addition & 0 deletions src/lib/libsigs.js
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,7 @@ sigs = {
_emval_decref__sig: 'vp',
_emval_delete__sig: 'ipp',
_emval_equals__sig: 'ipp',
_emval_from_current_cxa_exception__sig: 'p',
_emval_get_global__sig: 'pp',
_emval_get_method_caller__sig: 'pipi',
_emval_get_module_property__sig: 'pp',
Expand Down
41 changes: 35 additions & 6 deletions system/include/emscripten/val.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@
#include <pthread.h>
#if __cplusplus >= 202002L
#include <coroutine>
#include <exception>
#include <variant>
#endif


namespace emscripten {

class val;
Expand Down Expand Up @@ -118,6 +118,7 @@ EM_VAL _emval_iter_next(EM_VAL iterator);

#if __cplusplus >= 202002L
void _emval_coro_suspend(EM_VAL promise, void* coro_ptr);
EM_VAL _emval_from_current_cxa_exception();
EM_VAL _emval_coro_make_promise(EM_VAL *resolve, EM_VAL *reject);
#endif

Expand Down Expand Up @@ -729,7 +730,8 @@ class val::awaiter {
bool await_ready() { return false; }

// On suspend, store the coroutine handle and invoke a helper that will do
// a rough equivalent of `promise.then(value => this.resume_with(value))`.
// a rough equivalent of
// `promise.then(value => this.resume_with(value)).catch(error => this.reject_with(error))`.
void await_suspend(std::coroutine_handle<val::promise_type> handle) {
internal::_emval_coro_suspend(std::get<STATE_PROMISE>(state).as_handle(), this);
state.emplace<STATE_CORO>(handle);
Expand All @@ -743,9 +745,16 @@ class val::awaiter {
coro.resume();
}

// When JS invokes `reject_with` with some error value, reject currently suspended
// coroutine's promise with the error value and destroy coroutine frame, because
// in this scenario coroutine never reaches final_suspend point to be destroyed automatically.
void reject_with(val&& error);

// `await_resume` finalizes the awaiter and should return the result
// of the `co_await ...` expression - in our case, the stored value.
val await_resume() { return std::move(std::get<STATE_RESULT>(state)); }
val await_resume() {
return std::move(std::get<STATE_RESULT>(state));
}
};

inline val::awaiter val::operator co_await() const {
Expand All @@ -756,7 +765,7 @@ inline val::awaiter val::operator co_await() const {
// that compiler uses to drive the coroutine itself
// (`T::promise_type` is used for any coroutine with declared return type `T`).
class val::promise_type {
val promise, resolve, reject_with_current_exception;
val promise, resolve, reject;

public:
// Create a `new Promise` and store it alongside the `resolve` and `reject`
Expand All @@ -766,7 +775,7 @@ class val::promise_type {
EM_VAL reject_handle;
promise = val(internal::_emval_coro_make_promise(&resolve_handle, &reject_handle));
resolve = val(resolve_handle);
reject_with_current_exception = val(reject_handle);
reject = val(reject_handle);
}

// Return the stored promise as the actual return value of the coroutine.
Expand All @@ -779,7 +788,19 @@ class val::promise_type {
// On an unhandled exception, reject the stored promise instead of throwing
// it asynchronously where it can't be handled.
void unhandled_exception() {
reject_with_current_exception();
try {
std::rethrow_exception(std::current_exception());
} catch (const val& error) {
reject(error);
} catch (...) {
val error = val(internal::_emval_from_current_cxa_exception());
reject(error);
}
}

// Reject the stored promise due to rejection deeper in the call chain
void reject_with(val&& error) {
reject(std::move(error));
}

// Resolve the stored promise on `co_return value`.
Expand All @@ -788,6 +809,14 @@ class val::promise_type {
resolve(std::forward<T>(value));
}
};

inline void val::awaiter::reject_with(val&& error) {
auto coro = std::move(std::get<STATE_CORO>(state));
auto& promise = coro.promise();
promise.reject_with(std::move(error));
coro.destroy();
}

#endif

// Declare a custom type that can be used in conjunction with
Expand Down
4 changes: 4 additions & 0 deletions system/lib/embind/bind.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ void _emval_coro_resume(val::awaiter* awaiter, EM_VAL result) {
awaiter->resume_with(val::take_ownership(result));
}

void _emval_coro_reject(val::awaiter* awaiter, EM_VAL error) {
awaiter->reject_with(val::take_ownership(error));
}

}

namespace {
Expand Down
44 changes: 42 additions & 2 deletions test/embind/test_val_coro.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,24 @@ EM_JS(EM_VAL, promise_sleep_impl, (int ms, int result), {
return handle;
});

EM_JS(EM_VAL, promise_fail_impl, (), {
let promise = new Promise((_, reject) => setTimeout(reject, 1, new Error("bang from JS promise!")));
let handle = Emval.toHandle(promise);
// FIXME. See https://github.com/emscripten-core/emscripten/issues/16975.
#if __wasm64__
handle = BigInt(handle);
#endif
return handle;
});

val promise_sleep(int ms, int result = 0) {
return val::take_ownership(promise_sleep_impl(ms, result));
}

val promise_fail() {
return val::take_ownership(promise_fail_impl());
}

// Test that we can subclass and make custom awaitable types.
template <typename T>
class typed_promise: public val {
Expand All @@ -37,7 +51,13 @@ class typed_promise: public val {
}
};

template <size_t N>
val asyncCoro() {
co_return co_await asyncCoro<N - 1>();
}

template <>
val asyncCoro<0>() {
// check that just sleeping works
co_await promise_sleep(1);
// check that sleeping and receiving value works
Expand All @@ -50,12 +70,32 @@ val asyncCoro() {
co_return 34;
}

template <size_t N>
val throwingCoro() {
co_await throwingCoro<N - 1>();
co_return 56;
}

template <>
val throwingCoro<0>() {
throw std::runtime_error("bang from throwingCoro!");
co_return 56;
}

template <size_t N>
val failingPromise() {
co_await failingPromise<N - 1>();
co_return 65;
}

template <>
val failingPromise<0>() {
co_await promise_fail();
co_return 65;
}

EMSCRIPTEN_BINDINGS(test_val_coro) {
function("asyncCoro", asyncCoro);
function("throwingCoro", throwingCoro);
function("asyncCoro", asyncCoro<3>);
function("throwingCoro", throwingCoro<3>);
function("failingPromise", failingPromise<3>);
}
13 changes: 12 additions & 1 deletion test/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -7480,7 +7480,7 @@ def test_embind_val_coro(self):
self.emcc_args += ['-std=c++20', '--bind', '--post-js=post.js']
self.do_runf('embind/test_val_coro.cpp', '34\n')

def test_embind_val_coro_caught(self):
def test_embind_val_coro_propogate_cpp_exception(self):
self.set_setting('EXCEPTION_STACK_TRACES')
create_file('post.js', r'''Module.onRuntimeInitialized = () => {
Module.throwingCoro().then(
Expand All @@ -7491,6 +7491,17 @@ def test_embind_val_coro_caught(self):
self.emcc_args += ['-std=c++20', '--bind', '--post-js=post.js', '-fexceptions']
self.do_runf('embind/test_val_coro.cpp', 'rejected with: std::runtime_error: bang from throwingCoro!\n')

def test_embind_val_coro_propogate_js_error(self):
self.set_setting('EXCEPTION_STACK_TRACES')
create_file('post.js', r'''Module.onRuntimeInitialized = () => {
Module.failingPromise().then(
console.log,
err => console.error(`rejected with: ${err.message}`)
);
}''')
self.emcc_args += ['-std=c++20', '--bind', '--post-js=post.js', '-fexceptions']
self.do_runf('embind/test_val_coro.cpp', 'rejected with: bang from JS promise!\n')

def test_embind_dynamic_initialization(self):
self.emcc_args += ['-lembind']
self.do_run_in_out_file_test('embind/test_dynamic_initialization.cpp')
Expand Down
1 change: 1 addition & 0 deletions tools/emscripten.py
Original file line number Diff line number Diff line change
Expand Up @@ -1166,6 +1166,7 @@ def create_pointer_conversion_wrappers(metadata):
'emscripten_proxy_finish': '_p',
'emscripten_proxy_execute_queue': '_p',
'_emval_coro_resume': '_pp',
'_emval_coro_reject': '_pp',
'emscripten_main_runtime_thread_id': 'p',
'_emscripten_set_offscreencanvas_size_on_thread': '_pp__',
'fileno': '_p',
Expand Down
2 changes: 1 addition & 1 deletion tools/system_libs.py
Original file line number Diff line number Diff line change
Expand Up @@ -1927,7 +1927,7 @@ class libwebgpu_cpp(MTLibrary):
src_files = ['webgpu_cpp.cpp']


class libembind(Library):
class libembind(MTLibrary):
name = 'libembind'
never_force = True

Expand Down