diff --git a/src/module_wrap.cc b/src/module_wrap.cc index 6f8c9d10955b88..8be6dbd1d0262c 100644 --- a/src/module_wrap.cc +++ b/src/module_wrap.cc @@ -663,6 +663,22 @@ void ModuleWrap::EvaluateSync(const FunctionCallbackInfo& args) { CHECK(result->IsPromise()); Local promise = result.As(); if (promise->State() == Promise::PromiseState::kRejected) { + // The rejected promise is created by V8, so we don't get a chance to mark + // it as resolved before the rejection happens from evaluation. But we can + // tell the promise rejection callback to treat it as a promise rejected + // before handler was added which would remove it from the unhandled + // rejection handling, since we are converting it into an error and throw + // from here directly. + Local type = v8::Integer::New( + isolate, + static_cast( + v8::PromiseRejectEvent::kPromiseHandlerAddedAfterReject)); + Local args[] = {type, promise, Undefined(isolate)}; + if (env->promise_reject_callback() + ->Call(context, Undefined(isolate), arraysize(args), args) + .IsEmpty()) { + return; + } Local exception = promise->Result(); Local message = v8::Exception::CreateMessage(isolate, exception); diff --git a/test/es-module/test-require-module-error-catching.js b/test/es-module/test-require-module-error-catching.js new file mode 100644 index 00000000000000..c314513d9bbf04 --- /dev/null +++ b/test/es-module/test-require-module-error-catching.js @@ -0,0 +1,21 @@ +// This tests synchronous errors in ESM from require(esm) can be caught. + +'use strict'; + +require('../common'); +const assert = require('assert'); + +// Runtime errors from throw should be caught. +assert.throws(() => { + require('../fixtures/es-modules/runtime-error-esm.js'); +}, { + message: 'hello' +}); + +// References errors should be caught too. +assert.throws(() => { + require('../fixtures/es-modules/reference-error-esm.js'); +}, { + name: 'ReferenceError', + message: 'exports is not defined' +}); diff --git a/test/es-module/test-require-module-synchronous-rejection-handling.js b/test/es-module/test-require-module-synchronous-rejection-handling.js new file mode 100644 index 00000000000000..76a25e742bb900 --- /dev/null +++ b/test/es-module/test-require-module-synchronous-rejection-handling.js @@ -0,0 +1,13 @@ +// This synchronous rejections from require(esm) still go to the unhandled rejection +// handler. + +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +process.on('unhandledRejection', common.mustCall((reason, promise) => { + assert.strictEqual(reason, 'reject!'); +})); + +require('../fixtures/es-modules/synchronous-rejection-esm.js'); diff --git a/test/fixtures/es-modules/reference-error-esm.js b/test/fixtures/es-modules/reference-error-esm.js new file mode 100644 index 00000000000000..baf773c78970ad --- /dev/null +++ b/test/fixtures/es-modules/reference-error-esm.js @@ -0,0 +1,5 @@ +// This module is invalid in both ESM and CJS, because +// 'exports' are not defined in ESM, while require cannot be +// redeclared in CJS. +Object.defineProperty(exports, "__esModule", { value: true }); +const require = () => {}; diff --git a/test/fixtures/es-modules/runtime-error-esm.js b/test/fixtures/es-modules/runtime-error-esm.js new file mode 100644 index 00000000000000..1df3cc469adbfc --- /dev/null +++ b/test/fixtures/es-modules/runtime-error-esm.js @@ -0,0 +1,2 @@ +import 'node:fs'; // Forces it to be recognized as ESM. +throw new Error('hello'); diff --git a/test/fixtures/es-modules/synchronous-rejection-esm.js b/test/fixtures/es-modules/synchronous-rejection-esm.js new file mode 100644 index 00000000000000..34d066037e2140 --- /dev/null +++ b/test/fixtures/es-modules/synchronous-rejection-esm.js @@ -0,0 +1,2 @@ +import 'node:fs'; // Forces it to be recognized as ESM. +Promise.reject('reject!');