Repository: nodejs/node
Pull request: module: improve error message for top-level await in CommonJS

Added a specific error message for using top-level await in CommonJS modules. The error now suggests using ESM or wrapping await in an async function for clarity.

Fixes: #55776

Changed files:
filename: lib/internal/modules/esm/module_job.js
patch: @@ -73,9 +73,16 @@ const isCommonJSGlobalLikeNotDefinedError = (errorMessage) =>
  * @param {string} url
  * @returns {void}
  */
-const explainCommonJSGlobalLikeNotDefinedError = (e, url) => {
+const explainCommonJSGlobalLikeNotDefinedError = (e, url, hasTopLevelAwait) => {
   if (e?.name === 'ReferenceError' &&
       isCommonJSGlobalLikeNotDefinedError(e.message)) {
+
+    if (hasTopLevelAwait) {
+      e.message = `Cannot determine intended module format because both require() and top-level await are present. If the code is intended to be CommonJS, wrap await in an async function. If the code is intended to be an ES module, replace require() with import.`;
+      e.code = 'ERR_AMBIGUOUS_MODULE_SYNTAX';
+      return;
+    }
+
     e.message += ' in ES module scope';
 
     if (StringPrototypeStartsWith(e.message, 'require ')) {
@@ -364,7 +371,7 @@ class ModuleJob extends ModuleJobBase {
     try {
       await this.module.evaluate(timeout, breakOnSigint);
     } catch (e) {
-      explainCommonJSGlobalLikeNotDefinedError(e, this.module.url);
+      explainCommonJSGlobalLikeNotDefinedError(e, this.module.url, this.module.hasTopLevelAwait());
       throw e;
     }
     return { __proto__: null, module: this.module };
@@ -498,7 +505,7 @@ class ModuleJobSync extends ModuleJobBase {
       const namespace = this.module.evaluateSync(filename, parentFilename);
       return { __proto__: null, module: this.module, namespace };
     } catch (e) {
-      explainCommonJSGlobalLikeNotDefinedError(e, this.module.url);
+      explainCommonJSGlobalLikeNotDefinedError(e, this.module.url, this.module.hasTopLevelAwait());
       throw e;
     }
   }

filename: src/module_wrap.cc
patch: @@ -876,6 +876,27 @@ void ModuleWrap::IsGraphAsync(const FunctionCallbackInfo<Value>& args) {
   args.GetReturnValue().Set(module->IsGraphAsync());
 }
 
+void ModuleWrap::HasTopLevelAwait(const FunctionCallbackInfo<Value>& args) {
+  Isolate* isolate = args.GetIsolate();
+  ModuleWrap* obj;
+  ASSIGN_OR_RETURN_UNWRAP(&obj, args.This());
+
+  Local<Module> module = obj->module_.Get(isolate);
+
+  // Check if module is valid
+  if (module.IsEmpty()) {
+    args.GetReturnValue().Set(false);
+    return;
+  }
+
+  // For source text modules, check if the graph is async
+  // For synthetic modules, it's always false
+  bool has_top_level_await =
+      module->IsSourceTextModule() && module->IsGraphAsync();
+
+  args.GetReturnValue().Set(has_top_level_await);
+}
+
 void ModuleWrap::GetError(const FunctionCallbackInfo<Value>& args) {
   Isolate* isolate = args.GetIsolate();
   ModuleWrap* obj;
@@ -1304,6 +1325,8 @@ void ModuleWrap::CreatePerIsolateProperties(IsolateData* isolate_data,
   SetProtoMethodNoSideEffect(isolate, tpl, "getNamespace", GetNamespace);
   SetProtoMethodNoSideEffect(isolate, tpl, "getStatus", GetStatus);
   SetProtoMethodNoSideEffect(isolate, tpl, "isGraphAsync", IsGraphAsync);
+  SetProtoMethodNoSideEffect(
+      isolate, tpl, "hasTopLevelAwait", HasTopLevelAwait);
   SetProtoMethodNoSideEffect(isolate, tpl, "getError", GetError);
   SetConstructorFunction(isolate, target, "ModuleWrap", tpl);
   isolate_data->set_module_wrap_constructor_template(tpl);
@@ -1366,6 +1389,7 @@ void ModuleWrap::RegisterExternalReferences(
   registry->Register(GetStatus);
   registry->Register(GetError);
   registry->Register(IsGraphAsync);
+  registry->Register(HasTopLevelAwait);
 
   registry->Register(CreateRequiredModuleFacade);
 

filename: src/module_wrap.h
patch: @@ -64,6 +64,7 @@ class ModuleWrap : public BaseObject {
   void MemoryInfo(MemoryTracker* tracker) const override {
     tracker->TrackField("resolve_cache", resolve_cache_);
   }
+  static void HasTopLevelAwait(const v8::FunctionCallbackInfo<v8::Value>& args);
 
   v8::Local<v8::Context> context() const;
   v8::Maybe<bool> CheckUnsettledTopLevelAwait();

filename: test/es-module/test-esm-detect-ambiguous.mjs
patch: @@ -281,7 +281,10 @@ describe('Module syntax detection', { concurrency: !process.env.TEST_PARALLEL },
         'const fs = require("node:fs"); await Promise.resolve();',
       ]);
 
-      match(stderr, /ReferenceError: require is not defined in ES module scope/);
+      match(
+        stderr,
+        /ReferenceError: Cannot determine intended module format because both require\(\) and top-level await are present\. If the code is intended to be CommonJS, wrap await in an async function\. If the code is intended to be an ES module, replace require\(\) with import\./
+      );
       strictEqual(stdout, '');
       strictEqual(code, 1);
       strictEqual(signal, null);
@@ -423,3 +426,24 @@ describe('when working with Worker threads', () => {
     strictEqual(signal, null);
   });
 });
+
+describe('cjs & esm ambiguous syntax case', () => {
+  it('should throw an ambiguous syntax error when using top-level await with require', async () => {
+    const { stderr, code, signal } = await spawnPromisified(
+      process.execPath,
+      [
+        '--input-type=module',
+        '--eval',
+        `await 1;\nconst fs = require('fs');`,
+      ]
+    );
+
+    match(
+      stderr,
+      /ReferenceError: Cannot determine intended module format because both require\(\) and top-level await are present\. If the code is intended to be CommonJS, wrap await in an async function\. If the code is intended to be an ES module, replace require\(\) with import\./
+    );
+
+    strictEqual(code, 1);
+    strictEqual(signal, null);
+  });
+});