From 96874e8fbc5dc3da3c60ee26b598d9a72af46c57 Mon Sep 17 00:00:00 2001
From: Chengzhong Wu <legendecas@gmail.com>
Date: Mon, 25 Sep 2023 12:23:58 +0800
Subject: [PATCH] node-api: enable uncaught exceptions policy by default

This enables the option `--force-node-api-uncaught-exceptions-policy`
for a specific Node-API addon when it is compiled with
`NAPI_EXPERIMENTAL` (and this would be the default behavior when
`NAPI_VERSION` 10 releases). This would not break existing Node-API
addons.

PR-URL: https://github.com/nodejs/node/pull/49313
Refs: https://github.com/nodejs/node/pull/36510
Reviewed-By: Michael Dawson <midawson@redhat.com>
Reviewed-By: Gabriel Schulhof <gabrielschulhof@gmail.com>
---
 doc/api/n-api.md                              |  8 ++
 src/node_api.cc                               | 16 ++--
 test/js-native-api/test_reference/binding.gyp |  6 ++
 .../test_reference/test_finalizer.c           | 71 ++++++++++++++++++
 .../test_reference/test_finalizer.js          |  4 +-
 .../test_reference/test_reference.c           | 75 +++++--------------
 test/node-api/test_buffer/binding.gyp         |  4 +
 test/node-api/test_buffer/test_buffer.c       | 50 ++-----------
 test/node-api/test_buffer/test_finalizer.c    | 61 +++++++++++++++
 test/node-api/test_buffer/test_finalizer.js   |  2 +-
 .../test_threadsafe_function/binding.gyp      | 14 ++++
 ...n.js => test_legacy_uncaught_exception.js} |  2 +-
 .../test_uncaught_exception.c                 | 62 +++++++++++++++
 .../test_uncaught_exception.js                | 26 +------
 .../test_uncaught_exception_v9.js             |  8 ++
 .../uncaught_exception.js                     | 31 ++++++++
 16 files changed, 307 insertions(+), 133 deletions(-)
 create mode 100644 test/js-native-api/test_reference/test_finalizer.c
 create mode 100644 test/node-api/test_buffer/test_finalizer.c
 rename test/node-api/test_threadsafe_function/{test_force_uncaught_exception.js => test_legacy_uncaught_exception.js} (85%)
 create mode 100644 test/node-api/test_threadsafe_function/test_uncaught_exception.c
 create mode 100644 test/node-api/test_threadsafe_function/test_uncaught_exception_v9.js
 create mode 100644 test/node-api/test_threadsafe_function/uncaught_exception.js

diff --git a/doc/api/n-api.md b/doc/api/n-api.md
index 10b6dac61c4490..feb9fa6061ca4e 100644
--- a/doc/api/n-api.md
+++ b/doc/api/n-api.md
@@ -6245,6 +6245,13 @@ napi_create_threadsafe_function(napi_env env,
   [`napi_threadsafe_function_call_js`][] provides more details.
 * `[out] result`: The asynchronous thread-safe JavaScript function.
 
+**Change History:**
+
+* Experimental (`NAPI_EXPERIMENTAL` is defined):
+
+  Uncaught exceptions thrown in `call_js_cb` are handled with the
+  [`'uncaughtException'`][] event, instead of being ignored.
+
 ### `napi_get_threadsafe_function_context`
 
 <!-- YAML
@@ -6475,6 +6482,7 @@ the add-on's file name during loading.
 [Visual Studio]: https://visualstudio.microsoft.com
 [Working with JavaScript properties]: #working-with-javascript-properties
 [Xcode]: https://developer.apple.com/xcode/
+[`'uncaughtException'`]: process.md#event-uncaughtexception
 [`Number.MAX_SAFE_INTEGER`]: https://tc39.github.io/ecma262/#sec-number.max_safe_integer
 [`Number.MIN_SAFE_INTEGER`]: https://tc39.github.io/ecma262/#sec-number.min_safe_integer
 [`Worker`]: worker_threads.md#class-worker
diff --git a/src/node_api.cc b/src/node_api.cc
index 7537dc20b2bd82..368f05f3f4a261 100644
--- a/src/node_api.cc
+++ b/src/node_api.cc
@@ -82,9 +82,8 @@ void node_napi_env__::trigger_fatal_exception(v8::Local<v8::Value> local_err) {
   node::errors::TriggerUncaughtException(isolate, local_err, local_msg);
 }
 
-// option enforceUncaughtExceptionPolicy is added for not breaking existing
-// running n-api add-ons, and should be deprecated in the next major Node.js
-// release.
+// The option enforceUncaughtExceptionPolicy is added for not breaking existing
+// running Node-API add-ons.
 template <bool enforceUncaughtExceptionPolicy, typename T>
 void node_napi_env__::CallbackIntoModule(T&& call) {
   CallIntoModule(call, [](napi_env env_, v8::Local<v8::Value> local_err) {
@@ -93,19 +92,24 @@ void node_napi_env__::CallbackIntoModule(T&& call) {
       return;
     }
     node::Environment* node_env = env->node_env();
-    if (!node_env->options()->force_node_api_uncaught_exceptions_policy &&
+    // If the module api version is less than NAPI_VERSION_EXPERIMENTAL,
+    // and the option --force-node-api-uncaught-exceptions-policy is not
+    // specified, emit a warning about the uncaught exception instead of
+    // triggering uncaught exception event.
+    if (env->module_api_version < NAPI_VERSION_EXPERIMENTAL &&
+        !node_env->options()->force_node_api_uncaught_exceptions_policy &&
         !enforceUncaughtExceptionPolicy) {
       ProcessEmitDeprecationWarning(
           node_env,
           "Uncaught N-API callback exception detected, please run node "
-          "with option --force-node-api-uncaught-exceptions-policy=true"
+          "with option --force-node-api-uncaught-exceptions-policy=true "
           "to handle those exceptions properly.",
           "DEP0168");
       return;
     }
     // If there was an unhandled exception in the complete callback,
     // report it as a fatal exception. (There is no JavaScript on the
-    // callstack that can possibly handle it.)
+    // call stack that can possibly handle it.)
     env->trigger_fatal_exception(local_err);
   });
 }
diff --git a/test/js-native-api/test_reference/binding.gyp b/test/js-native-api/test_reference/binding.gyp
index d8940028915f15..a9d81ef9d2c05d 100644
--- a/test/js-native-api/test_reference/binding.gyp
+++ b/test/js-native-api/test_reference/binding.gyp
@@ -5,6 +5,12 @@
       "sources": [
         "test_reference.c"
       ]
+    },
+    {
+      "target_name": "test_finalizer",
+      "sources": [
+        "test_finalizer.c"
+      ]
     }
   ]
 }
diff --git a/test/js-native-api/test_reference/test_finalizer.c b/test/js-native-api/test_reference/test_finalizer.c
new file mode 100644
index 00000000000000..51492d9623f69c
--- /dev/null
+++ b/test/js-native-api/test_reference/test_finalizer.c
@@ -0,0 +1,71 @@
+#include <assert.h>
+#include <js_native_api.h>
+#include <stdlib.h>
+#include "../common.h"
+#include "../entry_point.h"
+
+static int test_value = 1;
+static int finalize_count = 0;
+
+static void FinalizeExternalCallJs(napi_env env, void* data, void* hint) {
+  int* actual_value = data;
+  NODE_API_ASSERT_RETURN_VOID(
+      env,
+      actual_value == &test_value,
+      "The correct pointer was passed to the finalizer");
+
+  napi_ref finalizer_ref = (napi_ref)hint;
+  napi_value js_finalizer;
+  napi_value recv;
+  NODE_API_CALL_RETURN_VOID(
+      env, napi_get_reference_value(env, finalizer_ref, &js_finalizer));
+  NODE_API_CALL_RETURN_VOID(env, napi_get_global(env, &recv));
+  NODE_API_CALL_RETURN_VOID(
+      env, napi_call_function(env, recv, js_finalizer, 0, NULL, NULL));
+  NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, finalizer_ref));
+}
+
+static napi_value CreateExternalWithJsFinalize(napi_env env,
+                                               napi_callback_info info) {
+  size_t argc = 1;
+  napi_value args[1];
+  NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL));
+  NODE_API_ASSERT(env, argc == 1, "Wrong number of arguments");
+  napi_value finalizer = args[0];
+  napi_valuetype finalizer_valuetype;
+  NODE_API_CALL(env, napi_typeof(env, finalizer, &finalizer_valuetype));
+  NODE_API_ASSERT(env,
+                  finalizer_valuetype == napi_function,
+                  "Wrong type of first argument");
+  napi_ref finalizer_ref;
+  NODE_API_CALL(env, napi_create_reference(env, finalizer, 1, &finalizer_ref));
+
+  napi_value result;
+  NODE_API_CALL(env,
+                napi_create_external(env,
+                                     &test_value,
+                                     FinalizeExternalCallJs,
+                                     finalizer_ref, /* finalize_hint */
+                                     &result));
+
+  finalize_count = 0;
+  return result;
+}
+
+EXTERN_C_START
+napi_value Init(napi_env env, napi_value exports) {
+  napi_property_descriptor descriptors[] = {
+      DECLARE_NODE_API_PROPERTY("createExternalWithJsFinalize",
+                                CreateExternalWithJsFinalize),
+  };
+
+  NODE_API_CALL(
+      env,
+      napi_define_properties(env,
+                             exports,
+                             sizeof(descriptors) / sizeof(*descriptors),
+                             descriptors));
+
+  return exports;
+}
+EXTERN_C_END
diff --git a/test/js-native-api/test_reference/test_finalizer.js b/test/js-native-api/test_reference/test_finalizer.js
index b70582fd0342fe..a5270512dc87c1 100644
--- a/test/js-native-api/test_reference/test_finalizer.js
+++ b/test/js-native-api/test_reference/test_finalizer.js
@@ -2,7 +2,7 @@
 // Flags: --expose-gc --force-node-api-uncaught-exceptions-policy
 
 const common = require('../../common');
-const test_reference = require(`./build/${common.buildType}/test_reference`);
+const binding = require(`./build/${common.buildType}/test_finalizer`);
 const assert = require('assert');
 
 process.on('uncaughtException', common.mustCall((err) => {
@@ -11,7 +11,7 @@ process.on('uncaughtException', common.mustCall((err) => {
 
 (async function() {
   {
-    test_reference.createExternalWithJsFinalize(
+    binding.createExternalWithJsFinalize(
       common.mustCall(() => {
         throw new Error('finalizer error');
       }));
diff --git a/test/js-native-api/test_reference/test_reference.c b/test/js-native-api/test_reference/test_reference.c
index 82c1f17d9dce0e..058be07363588b 100644
--- a/test/js-native-api/test_reference/test_reference.c
+++ b/test/js-native-api/test_reference/test_reference.c
@@ -22,20 +22,6 @@ static void FinalizeExternal(napi_env env, void* data, void* hint) {
   finalize_count++;
 }
 
-static void FinalizeExternalCallJs(napi_env env, void* data, void* hint) {
-  int *actual_value = data;
-  NODE_API_ASSERT_RETURN_VOID(env, actual_value == &test_value,
-      "The correct pointer was passed to the finalizer");
-
-  napi_ref finalizer_ref = (napi_ref)hint;
-  napi_value js_finalizer;
-  napi_value recv;
-  NODE_API_CALL_RETURN_VOID(env, napi_get_reference_value(env, finalizer_ref, &js_finalizer));
-  NODE_API_CALL_RETURN_VOID(env, napi_get_global(env, &recv));
-  NODE_API_CALL_RETURN_VOID(env, napi_call_function(env, recv, js_finalizer, 0, NULL, NULL));
-  NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, finalizer_ref));
-}
-
 static napi_value CreateExternal(napi_env env, napi_callback_info info) {
   int* data = &test_value;
 
@@ -118,31 +104,6 @@ CreateExternalWithFinalize(napi_env env, napi_callback_info info) {
   return result;
 }
 
-static napi_value
-CreateExternalWithJsFinalize(napi_env env, napi_callback_info info) {
-  size_t argc = 1;
-  napi_value args[1];
-  NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL));
-  NODE_API_ASSERT(env, argc == 1, "Wrong number of arguments");
-  napi_value finalizer = args[0];
-  napi_valuetype finalizer_valuetype;
-  NODE_API_CALL(env, napi_typeof(env, finalizer, &finalizer_valuetype));
-  NODE_API_ASSERT(env, finalizer_valuetype == napi_function, "Wrong type of first argument");
-  napi_ref finalizer_ref;
-  NODE_API_CALL(env, napi_create_reference(env, finalizer, 1, &finalizer_ref));
-
-  napi_value result;
-  NODE_API_CALL(env,
-      napi_create_external(env,
-                           &test_value,
-                           FinalizeExternalCallJs,
-                           finalizer_ref, /* finalize_hint */
-                           &result));
-
-  finalize_count = 0;
-  return result;
-}
-
 static napi_value CheckExternal(napi_env env, napi_callback_info info) {
   size_t argc = 1;
   napi_value arg;
@@ -263,24 +224,24 @@ static napi_value ValidateDeleteBeforeFinalize(napi_env env, napi_callback_info
 EXTERN_C_START
 napi_value Init(napi_env env, napi_value exports) {
   napi_property_descriptor descriptors[] = {
-    DECLARE_NODE_API_GETTER("finalizeCount", GetFinalizeCount),
-    DECLARE_NODE_API_PROPERTY("createExternal", CreateExternal),
-    DECLARE_NODE_API_PROPERTY("createExternalWithFinalize",
-        CreateExternalWithFinalize),
-    DECLARE_NODE_API_PROPERTY("createExternalWithJsFinalize",
-        CreateExternalWithJsFinalize),
-    DECLARE_NODE_API_PROPERTY("checkExternal", CheckExternal),
-    DECLARE_NODE_API_PROPERTY("createReference", CreateReference),
-    DECLARE_NODE_API_PROPERTY("createSymbol", CreateSymbol),
-    DECLARE_NODE_API_PROPERTY("createSymbolFor", CreateSymbolFor),
-    DECLARE_NODE_API_PROPERTY("createSymbolForEmptyString", CreateSymbolForEmptyString),
-    DECLARE_NODE_API_PROPERTY("createSymbolForIncorrectLength", CreateSymbolForIncorrectLength),
-    DECLARE_NODE_API_PROPERTY("deleteReference", DeleteReference),
-    DECLARE_NODE_API_PROPERTY("incrementRefcount", IncrementRefcount),
-    DECLARE_NODE_API_PROPERTY("decrementRefcount", DecrementRefcount),
-    DECLARE_NODE_API_GETTER("referenceValue", GetReferenceValue),
-    DECLARE_NODE_API_PROPERTY("validateDeleteBeforeFinalize",
-                          ValidateDeleteBeforeFinalize),
+      DECLARE_NODE_API_GETTER("finalizeCount", GetFinalizeCount),
+      DECLARE_NODE_API_PROPERTY("createExternal", CreateExternal),
+      DECLARE_NODE_API_PROPERTY("createExternalWithFinalize",
+                                CreateExternalWithFinalize),
+      DECLARE_NODE_API_PROPERTY("checkExternal", CheckExternal),
+      DECLARE_NODE_API_PROPERTY("createReference", CreateReference),
+      DECLARE_NODE_API_PROPERTY("createSymbol", CreateSymbol),
+      DECLARE_NODE_API_PROPERTY("createSymbolFor", CreateSymbolFor),
+      DECLARE_NODE_API_PROPERTY("createSymbolForEmptyString",
+                                CreateSymbolForEmptyString),
+      DECLARE_NODE_API_PROPERTY("createSymbolForIncorrectLength",
+                                CreateSymbolForIncorrectLength),
+      DECLARE_NODE_API_PROPERTY("deleteReference", DeleteReference),
+      DECLARE_NODE_API_PROPERTY("incrementRefcount", IncrementRefcount),
+      DECLARE_NODE_API_PROPERTY("decrementRefcount", DecrementRefcount),
+      DECLARE_NODE_API_GETTER("referenceValue", GetReferenceValue),
+      DECLARE_NODE_API_PROPERTY("validateDeleteBeforeFinalize",
+                                ValidateDeleteBeforeFinalize),
   };
 
   NODE_API_CALL(env, napi_define_properties(
diff --git a/test/node-api/test_buffer/binding.gyp b/test/node-api/test_buffer/binding.gyp
index e41a3993cd7c9d..e5d0955ae6308e 100644
--- a/test/node-api/test_buffer/binding.gyp
+++ b/test/node-api/test_buffer/binding.gyp
@@ -3,6 +3,10 @@
     {
       "target_name": "test_buffer",
       "sources": [ "test_buffer.c" ]
+    },
+    {
+      "target_name": "test_finalizer",
+      "sources": [ "test_finalizer.c" ]
     }
   ]
 }
diff --git a/test/node-api/test_buffer/test_buffer.c b/test/node-api/test_buffer/test_buffer.c
index bc61cd7a2e9062..013a7e2d417fbe 100644
--- a/test/node-api/test_buffer/test_buffer.c
+++ b/test/node-api/test_buffer/test_buffer.c
@@ -22,17 +22,6 @@ static void noopDeleter(napi_env env, void* data, void* finalize_hint) {
   deleterCallCount++;
 }
 
-static void malignDeleter(napi_env env, void* data, void* finalize_hint) {
-  NODE_API_ASSERT_RETURN_VOID(env, data != NULL && strcmp(data, theText) == 0, "invalid data");
-  napi_ref finalizer_ref = (napi_ref)finalize_hint;
-  napi_value js_finalizer;
-  napi_value recv;
-  NODE_API_CALL_RETURN_VOID(env, napi_get_reference_value(env, finalizer_ref, &js_finalizer));
-  NODE_API_CALL_RETURN_VOID(env, napi_get_global(env, &recv));
-  NODE_API_CALL_RETURN_VOID(env, napi_call_function(env, recv, js_finalizer, 0, NULL, NULL));
-  NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, finalizer_ref));
-}
-
 static napi_value newBuffer(napi_env env, napi_callback_info info) {
   napi_value theBuffer;
   char* theCopy;
@@ -118,30 +107,6 @@ static napi_value staticBuffer(napi_env env, napi_callback_info info) {
   return theBuffer;
 }
 
-static napi_value malignFinalizerBuffer(napi_env env, napi_callback_info info) {
-  size_t argc = 1;
-  napi_value args[1];
-  NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL));
-  NODE_API_ASSERT(env, argc == 1, "Wrong number of arguments");
-  napi_value finalizer = args[0];
-  napi_valuetype finalizer_valuetype;
-  NODE_API_CALL(env, napi_typeof(env, finalizer, &finalizer_valuetype));
-  NODE_API_ASSERT(env, finalizer_valuetype == napi_function, "Wrong type of first argument");
-  napi_ref finalizer_ref;
-  NODE_API_CALL(env, napi_create_reference(env, finalizer, 1, &finalizer_ref));
-
-  napi_value theBuffer;
-  NODE_API_CALL(
-      env,
-      napi_create_external_buffer(env,
-                                  sizeof(theText),
-                                  (void*)theText,
-                                  malignDeleter,
-                                  finalizer_ref,  // finalize_hint
-                                  &theBuffer));
-  return theBuffer;
-}
-
 static napi_value Init(napi_env env, napi_value exports) {
   napi_value theValue;
 
@@ -151,14 +116,13 @@ static napi_value Init(napi_env env, napi_value exports) {
       napi_set_named_property(env, exports, "theText", theValue));
 
   napi_property_descriptor methods[] = {
-    DECLARE_NODE_API_PROPERTY("newBuffer", newBuffer),
-    DECLARE_NODE_API_PROPERTY("newExternalBuffer", newExternalBuffer),
-    DECLARE_NODE_API_PROPERTY("getDeleterCallCount", getDeleterCallCount),
-    DECLARE_NODE_API_PROPERTY("copyBuffer", copyBuffer),
-    DECLARE_NODE_API_PROPERTY("bufferHasInstance", bufferHasInstance),
-    DECLARE_NODE_API_PROPERTY("bufferInfo", bufferInfo),
-    DECLARE_NODE_API_PROPERTY("staticBuffer", staticBuffer),
-    DECLARE_NODE_API_PROPERTY("malignFinalizerBuffer", malignFinalizerBuffer),
+      DECLARE_NODE_API_PROPERTY("newBuffer", newBuffer),
+      DECLARE_NODE_API_PROPERTY("newExternalBuffer", newExternalBuffer),
+      DECLARE_NODE_API_PROPERTY("getDeleterCallCount", getDeleterCallCount),
+      DECLARE_NODE_API_PROPERTY("copyBuffer", copyBuffer),
+      DECLARE_NODE_API_PROPERTY("bufferHasInstance", bufferHasInstance),
+      DECLARE_NODE_API_PROPERTY("bufferInfo", bufferInfo),
+      DECLARE_NODE_API_PROPERTY("staticBuffer", staticBuffer),
   };
 
   NODE_API_CALL(env, napi_define_properties(
diff --git a/test/node-api/test_buffer/test_finalizer.c b/test/node-api/test_buffer/test_finalizer.c
new file mode 100644
index 00000000000000..eb5426d8f29cdf
--- /dev/null
+++ b/test/node-api/test_buffer/test_finalizer.c
@@ -0,0 +1,61 @@
+#include <node_api.h>
+#include <stdlib.h>
+#include <string.h>
+#include "../../js-native-api/common.h"
+
+static const char theText[] =
+    "Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
+
+static void malignDeleter(napi_env env, void* data, void* finalize_hint) {
+  NODE_API_ASSERT_RETURN_VOID(
+      env, data != NULL && strcmp(data, theText) == 0, "invalid data");
+  napi_ref finalizer_ref = (napi_ref)finalize_hint;
+  napi_value js_finalizer;
+  napi_value recv;
+  NODE_API_CALL_RETURN_VOID(
+      env, napi_get_reference_value(env, finalizer_ref, &js_finalizer));
+  NODE_API_CALL_RETURN_VOID(env, napi_get_global(env, &recv));
+  NODE_API_CALL_RETURN_VOID(
+      env, napi_call_function(env, recv, js_finalizer, 0, NULL, NULL));
+  NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, finalizer_ref));
+}
+
+static napi_value malignFinalizerBuffer(napi_env env, napi_callback_info info) {
+  size_t argc = 1;
+  napi_value args[1];
+  NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL));
+  NODE_API_ASSERT(env, argc == 1, "Wrong number of arguments");
+  napi_value finalizer = args[0];
+  napi_valuetype finalizer_valuetype;
+  NODE_API_CALL(env, napi_typeof(env, finalizer, &finalizer_valuetype));
+  NODE_API_ASSERT(env,
+                  finalizer_valuetype == napi_function,
+                  "Wrong type of first argument");
+  napi_ref finalizer_ref;
+  NODE_API_CALL(env, napi_create_reference(env, finalizer, 1, &finalizer_ref));
+
+  napi_value theBuffer;
+  NODE_API_CALL(env,
+                napi_create_external_buffer(env,
+                                            sizeof(theText),
+                                            (void*)theText,
+                                            malignDeleter,
+                                            finalizer_ref,  // finalize_hint
+                                            &theBuffer));
+  return theBuffer;
+}
+
+static napi_value Init(napi_env env, napi_value exports) {
+  napi_property_descriptor methods[] = {
+      DECLARE_NODE_API_PROPERTY("malignFinalizerBuffer", malignFinalizerBuffer),
+  };
+
+  NODE_API_CALL(
+      env,
+      napi_define_properties(
+          env, exports, sizeof(methods) / sizeof(methods[0]), methods));
+
+  return exports;
+}
+
+NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)
diff --git a/test/node-api/test_buffer/test_finalizer.js b/test/node-api/test_buffer/test_finalizer.js
index b706c68c7c3e02..35511fcb016c95 100644
--- a/test/node-api/test_buffer/test_finalizer.js
+++ b/test/node-api/test_buffer/test_finalizer.js
@@ -2,7 +2,7 @@
 // Flags: --expose-gc --force-node-api-uncaught-exceptions-policy
 
 const common = require('../../common');
-const binding = require(`./build/${common.buildType}/test_buffer`);
+const binding = require(`./build/${common.buildType}/test_finalizer`);
 const assert = require('assert');
 const tick = require('util').promisify(require('../../common/tick'));
 
diff --git a/test/node-api/test_threadsafe_function/binding.gyp b/test/node-api/test_threadsafe_function/binding.gyp
index b60352e05af103..58a9d04d4a5619 100644
--- a/test/node-api/test_threadsafe_function/binding.gyp
+++ b/test/node-api/test_threadsafe_function/binding.gyp
@@ -3,6 +3,20 @@
     {
       'target_name': 'binding',
       'sources': ['binding.c']
+    },
+    {
+      'target_name': 'test_uncaught_exception_v9',
+      'defines': [
+        'NAPI_VERSION=9'
+      ],
+      'sources': ['test_uncaught_exception.c']
+    },
+    {
+      'target_name': 'test_uncaught_exception',
+      'defines': [
+        'NAPI_EXPERIMENTAL'
+      ],
+      'sources': ['test_uncaught_exception.c']
     }
   ]
 }
diff --git a/test/node-api/test_threadsafe_function/test_force_uncaught_exception.js b/test/node-api/test_threadsafe_function/test_legacy_uncaught_exception.js
similarity index 85%
rename from test/node-api/test_threadsafe_function/test_force_uncaught_exception.js
rename to test/node-api/test_threadsafe_function/test_legacy_uncaught_exception.js
index b1f95715eadf60..a8743e00b5b8c5 100644
--- a/test/node-api/test_threadsafe_function/test_force_uncaught_exception.js
+++ b/test/node-api/test_threadsafe_function/test_legacy_uncaught_exception.js
@@ -2,7 +2,7 @@
 // Flags: --no-force-node-api-uncaught-exceptions-policy
 
 const common = require('../../common');
-const binding = require(`./build/${common.buildType}/binding`);
+const binding = require(`./build/${common.buildType}/test_uncaught_exception_v9`);
 
 process.on(
   'uncaughtException',
diff --git a/test/node-api/test_threadsafe_function/test_uncaught_exception.c b/test/node-api/test_threadsafe_function/test_uncaught_exception.c
new file mode 100644
index 00000000000000..f8499d4fe4d680
--- /dev/null
+++ b/test/node-api/test_threadsafe_function/test_uncaught_exception.c
@@ -0,0 +1,62 @@
+#include <node_api.h>
+#include "../../js-native-api/common.h"
+
+// Testing calling into JavaScript
+static void ThreadSafeFunctionFinalize(napi_env env,
+                                       void* finalize_data,
+                                       void* finalize_hint) {
+  napi_ref js_func_ref = (napi_ref)finalize_data;
+  napi_value js_func;
+  napi_value recv;
+  NODE_API_CALL_RETURN_VOID(
+      env, napi_get_reference_value(env, js_func_ref, &js_func));
+  NODE_API_CALL_RETURN_VOID(env, napi_get_global(env, &recv));
+  NODE_API_CALL_RETURN_VOID(
+      env, napi_call_function(env, recv, js_func, 0, NULL, NULL));
+  NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, js_func_ref));
+}
+
+// Testing calling into JavaScript
+static napi_value CallIntoModule(napi_env env, napi_callback_info info) {
+  size_t argc = 4;
+  napi_value argv[4];
+  NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
+
+  napi_ref finalize_func;
+  NODE_API_CALL(env, napi_create_reference(env, argv[3], 1, &finalize_func));
+
+  napi_threadsafe_function tsfn;
+  NODE_API_CALL(env,
+                napi_create_threadsafe_function(env,
+                                                argv[0],
+                                                argv[1],
+                                                argv[2],
+                                                0,
+                                                1,
+                                                finalize_func,
+                                                ThreadSafeFunctionFinalize,
+                                                NULL,
+                                                NULL,
+                                                &tsfn));
+  NODE_API_CALL(env,
+                napi_call_threadsafe_function(tsfn, NULL, napi_tsfn_blocking));
+  NODE_API_CALL(env, napi_release_threadsafe_function(tsfn, napi_tsfn_release));
+  return NULL;
+}
+
+// Module init
+static napi_value Init(napi_env env, napi_value exports) {
+  napi_property_descriptor properties[] = {
+      DECLARE_NODE_API_PROPERTY("CallIntoModule", CallIntoModule),
+  };
+
+  NODE_API_CALL(
+      env,
+      napi_define_properties(env,
+                             exports,
+                             sizeof(properties) / sizeof(properties[0]),
+                             properties));
+
+  return exports;
+}
+NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)
diff --git a/test/node-api/test_threadsafe_function/test_uncaught_exception.js b/test/node-api/test_threadsafe_function/test_uncaught_exception.js
index 2529757908999b..81b2623d702790 100644
--- a/test/node-api/test_threadsafe_function/test_uncaught_exception.js
+++ b/test/node-api/test_threadsafe_function/test_uncaught_exception.js
@@ -1,27 +1,7 @@
 'use strict';
-// Flags: --force-node-api-uncaught-exceptions-policy
 
 const common = require('../../common');
-const assert = require('assert');
-const binding = require(`./build/${common.buildType}/binding`);
+const binding = require(`./build/${common.buildType}/test_uncaught_exception`);
+const { testUncaughtException } = require('./uncaught_exception');
 
-const callbackCheck = common.mustCall((err) => {
-  assert.throws(() => { throw err; }, /callback error/);
-  process.removeListener('uncaughtException', callbackCheck);
-  process.on('uncaughtException', finalizerCheck);
-});
-const finalizerCheck = common.mustCall((err) => {
-  assert.throws(() => { throw err; }, /finalizer error/);
-});
-process.on('uncaughtException', callbackCheck);
-
-binding.CallIntoModule(
-  common.mustCall(() => {
-    throw new Error('callback error');
-  }),
-  {},
-  'resource_name',
-  common.mustCall(function finalizer() {
-    throw new Error('finalizer error');
-  }),
-);
+testUncaughtException(binding);
diff --git a/test/node-api/test_threadsafe_function/test_uncaught_exception_v9.js b/test/node-api/test_threadsafe_function/test_uncaught_exception_v9.js
new file mode 100644
index 00000000000000..28e628918fdff2
--- /dev/null
+++ b/test/node-api/test_threadsafe_function/test_uncaught_exception_v9.js
@@ -0,0 +1,8 @@
+'use strict';
+// Flags: --force-node-api-uncaught-exceptions-policy
+
+const common = require('../../common');
+const binding = require(`./build/${common.buildType}/test_uncaught_exception_v9`);
+const { testUncaughtException } = require('./uncaught_exception');
+
+testUncaughtException(binding);
diff --git a/test/node-api/test_threadsafe_function/uncaught_exception.js b/test/node-api/test_threadsafe_function/uncaught_exception.js
new file mode 100644
index 00000000000000..da2aa2f4efef8a
--- /dev/null
+++ b/test/node-api/test_threadsafe_function/uncaught_exception.js
@@ -0,0 +1,31 @@
+'use strict';
+
+const common = require('../../common');
+const assert = require('assert');
+
+function testUncaughtException(binding) {
+  const callbackCheck = common.mustCall((err) => {
+    assert.throws(() => { throw err; }, /callback error/);
+    process.removeListener('uncaughtException', callbackCheck);
+    process.on('uncaughtException', finalizerCheck);
+  });
+  const finalizerCheck = common.mustCall((err) => {
+    assert.throws(() => { throw err; }, /finalizer error/);
+  });
+  process.on('uncaughtException', callbackCheck);
+
+  binding.CallIntoModule(
+    common.mustCall(() => {
+      throw new Error('callback error');
+    }),
+    {},
+    'resource_name',
+    common.mustCall(function finalizer() {
+      throw new Error('finalizer error');
+    }),
+  );
+}
+
+module.exports = {
+  testUncaughtException,
+};