Repository: denoland/deno
Pull request: fix(ext/node): emit error event for worker with missing main module

Fixes `test-worker-esm-missing-main.js` test

Changed files:
filename: ext/node/ops/worker_threads.rs
patch: @@ -86,21 +86,17 @@ pub fn op_worker_threads_filename<TSys: ExtNodeSys + 'static>(
     let path = ensure_read_permission(state, Cow::Borrowed(path))
       .map_err(WorkerThreadsFilenameError::Permission)?;
     let sys = state.borrow::<TSys>();
-    let canonicalized_path =
-      deno_path_util::strip_unc_prefix(sys.fs_canonicalize(&path)?);
+    let canonicalized_path = match sys.fs_canonicalize(&path) {
+      Ok(p) => deno_path_util::strip_unc_prefix(p),
+      Err(_) => path.to_path_buf(),
+    };
     Url::from_file_path(canonicalized_path)
       .map_err(|_| WorkerThreadsFilenameError::UrlFromPathString)?
   };
   let url_path = url
     .to_file_path()
     .map_err(|_| WorkerThreadsFilenameError::UrlToPathString)?;
-  let url_path = ensure_read_permission(state, Cow::Owned(url_path))
+  let _url_path = ensure_read_permission(state, Cow::Owned(url_path))
     .map_err(WorkerThreadsFilenameError::Permission)?;
-  let sys = state.borrow::<TSys>();
-  if !sys.fs_exists_no_err(&url_path) {
-    return Err(WorkerThreadsFilenameError::FileNotFound(
-      url_path.to_path_buf(),
-    ));
-  }
   Ok(Some(url.into()))
 }

filename: runtime/web_worker.rs
patch: @@ -439,6 +439,7 @@ pub struct WebWorker {
   maybe_worker_metadata: Option<WorkerMetadata>,
   memory_trim_handle: Option<tokio::task::JoinHandle<()>>,
   maybe_coverage_dir: Option<PathBuf>,
+  bootstrap_error: Option<CoreError>,
 }
 
 impl Drop for WebWorker {
@@ -749,6 +750,7 @@ impl WebWorker {
         maybe_worker_metadata: options.maybe_worker_metadata,
         memory_trim_handle: None,
         maybe_coverage_dir: options.maybe_coverage_dir,
+        bootstrap_error: None,
       },
       external_handle,
       options.bootstrap,
@@ -762,6 +764,7 @@ impl WebWorker {
     // WebWorkers can have empty string as name.
     {
       deno_core::scope!(scope, &mut self.js_runtime);
+      v8::tc_scope!(scope, scope);
       let args = options.as_v8(scope);
       let bootstrap_fn = self.bootstrap_fn_global.take().unwrap();
       let bootstrap_fn = v8::Local::new(scope, bootstrap_fn);
@@ -789,13 +792,26 @@ impl WebWorker {
         v8::Integer::new(scope, self.id.0 as i32).into();
       let worker_type: v8::Local<v8::Value> =
         self.worker_type.to_v8(scope).into();
-      bootstrap_fn
-        .call(
-          scope,
-          undefined.into(),
-          &[args, name_str, id_str, id, worker_type, worker_data],
-        )
-        .unwrap();
+      let result = bootstrap_fn.call(
+        scope,
+        undefined.into(),
+        &[args, name_str, id_str, id, worker_type, worker_data],
+      );
+
+      if result.is_none() {
+        let error: CoreError = match scope.exception() {
+          Some(exception) => {
+            let js_error =
+              deno_core::error::JsError::from_v8_exception(scope, exception);
+            CoreError::from(js_error)
+          }
+          None => CoreError::from(deno_error::JsErrorBox::generic(
+            "Bootstrap unexpectedly failed",
+          )),
+        };
+        self.bootstrap_error = Some(error);
+        return;
+      }
 
       let context = scope.get_current_context();
       let global = context.global(scope);
@@ -1081,6 +1097,15 @@ pub async fn run_web_worker(
   let name = worker.name.to_string();
   let internal_handle = worker.internal_handle.clone();
 
+  // If the bootstrap failed, report it as a terminal error.
+  if let Some(error) = worker.bootstrap_error.take() {
+    print_worker_error(&error, &name, format_js_error_fn.as_deref());
+    internal_handle
+      .post_event(WorkerControlEvent::TerminalError(error))
+      .expect("Failed to post message to host");
+    return Ok(());
+  }
+
   // Execute provided source code immediately
   let result = if let Some(source_code) = maybe_source_code.take() {
     let r = worker.execute_script(located_script_name!(), source_code.into());
@@ -1124,6 +1149,27 @@ pub async fn run_web_worker(
 
   if let Err(e) = result {
     print_worker_error(&e, &name, format_js_error_fn.as_deref());
+    // For Node workers, convert "Module not found" to Node-compatible
+    // "Cannot find module" format so it matches Node.js error behavior.
+    let e = if internal_handle.worker_type == WorkerThreadType::Node {
+      let msg = e.to_string();
+      if msg.starts_with("Module not found") {
+        let path = specifier.to_file_path().ok();
+        let display = path
+          .as_deref()
+          .map(|p| p.display().to_string())
+          .unwrap_or_else(|| specifier.to_string());
+        CoreErrorKind::JsBox(deno_error::JsErrorBox::new(
+          "Error",
+          format!("Cannot find module '{display}'"),
+        ))
+        .into_box()
+      } else {
+        e
+      }
+    } else {
+      e
+    };
     internal_handle
       .post_event(WorkerControlEvent::TerminalError(e))
       .expect("Failed to post message to host");

filename: tests/node_compat/config.jsonc
patch: @@ -1548,6 +1548,7 @@
     "parallel/test-worker-dispose.mjs": {},
     "parallel/test-worker-error-stack-getter-throws.js": {},
     "parallel/test-worker-esm-exit.js": {},
+    "parallel/test-worker-esm-missing-main.js": {},
     "parallel/test-worker-esmodule.js": {},
     "parallel/test-worker-event.js": {},
     "parallel/test-worker-execargv-invalid.js": {},

filename: tests/unit_node/worker_threads_test.ts
patch: @@ -282,17 +282,6 @@ Deno.test({
   },
 });
 
-Deno.test({
-  name: "[node/worker_threads] throws on non-existend file",
-  fn() {
-    assertThrows(
-      () => {
-        new workerThreads.Worker(new URL("file://very/unlikely"));
-      },
-    );
-  },
-});
-
 Deno.test({
   name: "[node/worker_threads] inheritances",
   async fn() {