Repository: denoland/deno
Pull request: fix(ext/node): `fs.unlink` and `fs.unlinkSync` compatibility

Improves compatibility with Node.js by validating the input parameters, accepting Buffer type paths, and converting thrown errors correctly. This PR also addresses `prefer-primordials` lint rule. These changes allow [parallel/test-fs-unlink-type-check.js.js](https://github.com/denoland/node_test/blob/8846b5392f4ebd3d4e995b9bed61c23c0192d806/test/parallel/test-fs-unlink-type-check.js) test to pass.

Towards https://github.com/denoland/deno/issues/29972, https://github.com/denoland/deno/issues/24236.


Given this code:
```js
import { unlink } from "node:fs/promises";

const main = async () => {
  try {
    await unlink("doesnt-exists");
  } catch (error) {
    console.error("Error code:", error.code);
    console.error("Error syscall:", error.syscall);
    console.error("Error path:", error.path);
  }
}

main()
```

These are the outputs:
```bash
# Node.js v24.4.1
node foo.js
Error code: ENOENT
Error syscall: unlink
Error path: doesnt-exists

# Deno 2.4.2
deno -A foo.js
Error code: ENOENT
Error syscall: undefined
Error path: undefined

# Deno (this PR)
./target/debug/deno -A foo.js
Error code: ENOENT
Error syscall: unlink
Error path: doesnt-exists
```

<!--
Before submitting a PR, please read https://docs.deno.com/runtime/manual/references/contributing

1. Give the PR a descriptive title.

  Examples of good title:
    - fix(std/http): Fix race condition in server
    - docs(console): Update docstrings
    - feat(doc): Handle nested reexports

  Examples of bad title:
    - fix #7123
    - update docs
    - fix bugs

2. Ensure there is a related issue and it is referenced in the PR text.
3. Ensure there are tests that cover the changes.
4. Ensure `cargo test` passes.
5. Ensure `./tools/format.js` passes without changing files.
6. Ensure `./tools/lint.js` passes.
7. Open as a draft PR if your work is still in progress. The CI won't run
   all steps, but you can add '[ci]' to a commit message to force it to.
8. If you would like to run the benchmarks on the CI, add the 'ci-bench' label.
-->


Changed files:
filename: ext/node/polyfills/_fs/_fs_unlink.ts
patch: @@ -1,19 +1,41 @@
 // Copyright 2018-2025 the Deno authors. MIT license.
 
-// TODO(petamoriken): enable prefer-primordials for node polyfills
-// deno-lint-ignore-file prefer-primordials
-
 import { promisify } from "ext:deno_node/internal/util.mjs";
+import type { Buffer } from "node:buffer";
+import { primordials } from "ext:core/mod.js";
+import { denoErrorToNodeError } from "ext:deno_node/internal/errors.ts";
+import { getValidatedPathToString } from "ext:deno_node/internal/fs/utils.mjs";
+import { validateFunction } from "ext:deno_node/internal/validators.mjs";
+import * as pathModule from "node:path";
+
+const {
+  PromisePrototypeThen,
+} = primordials;
+
+export function unlink(
+  path: string | Buffer | URL,
+  callback: (err?: Error) => void,
+): void {
+  path = getValidatedPathToString(path);
+  validateFunction(callback, "callback");
 
-export function unlink(path: string | URL, callback: (err?: Error) => void) {
-  if (!callback) throw new Error("No callback function supplied");
-  Deno.remove(path).then((_) => callback(), callback);
+  PromisePrototypeThen(
+    Deno.remove(pathModule.toNamespacedPath(path)),
+    () => callback(),
+    (err: Error) =>
+      callback(denoErrorToNodeError(err, { syscall: "unlink", path })),
+  );
 }
 
 export const unlinkPromise = promisify(unlink) as (
-  path: string | URL,
+  path: string | Buffer | URL,
 ) => Promise<void>;
 
-export function unlinkSync(path: string | URL) {
-  Deno.removeSync(path);
+export function unlinkSync(path: string | Buffer | URL): void {
+  path = getValidatedPathToString(path);
+  try {
+    Deno.removeSync(pathModule.toNamespacedPath(path));
+  } catch (err) {
+    throw denoErrorToNodeError(err as Error, { syscall: "unlink", path });
+  }
 }

filename: tests/node_compat/config.toml
patch: @@ -454,6 +454,7 @@
 "parallel/test-fs-rmdir-type-check.js" = {}
 "parallel/test-fs-stream-fs-options.js" = {}
 "parallel/test-fs-stream-options.js" = {}
+"parallel/test-fs-unlink-type-check.js" = {}
 "parallel/test-fs-util-validateoffsetlength.js" = {}
 "parallel/test-fs-write-negativeoffset.js" = {}
 "parallel/test-fs-write-no-fd.js" = {}

filename: tests/unit_node/_fs/_fs_unlink_test.ts
patch: @@ -1,8 +1,9 @@
 // Copyright 2018-2025 the Deno authors. MIT license.
 import { assertEquals, fail } from "@std/assert";
-import { existsSync } from "node:fs";
 import { assertCallbackErrorUncaught } from "../_test_utils.ts";
-import { unlink, unlinkSync } from "node:fs";
+import { existsSync, unlink, unlinkSync } from "node:fs";
+import { Buffer } from "node:buffer";
+import { join } from "@std/path";
 
 Deno.test({
   name: "ASYNC: deleting a file",
@@ -38,3 +39,56 @@ Deno.test("[std/node/fs] unlink callback isn't called twice if error is thrown",
     invocation: `unlink(${JSON.stringify(tempFile)}, `,
   });
 });
+
+Deno.test("[std/node/fs] unlink accepts Buffer path", async () => {
+  const file = Deno.makeTempFileSync();
+  const bufferPath = Buffer.from(file, "utf-8");
+  await new Promise<void>((resolve, reject) => {
+    unlink(bufferPath, (err) => {
+      if (err) reject(err);
+      resolve();
+    });
+  })
+    .then(() => assertEquals(existsSync(file), false), () => fail());
+});
+
+Deno.test("[std/node/fs] unlinkSync accepts Buffer path", () => {
+  const file = Deno.makeTempFileSync();
+  const bufferPath = Buffer.from(file, "utf-8");
+  unlinkSync(bufferPath);
+  assertEquals(existsSync(file), false);
+});
+
+Deno.test("[std/node/fs] unlink: convert Deno error to Node.js error", async () => {
+  const dir = Deno.makeTempDirSync();
+  const path = join(dir, "non_existent_file");
+
+  await new Promise<void>((resolve, reject) => {
+    unlink(path, (err) => {
+      if (err) reject(err);
+      resolve();
+    });
+  })
+    .then(() => fail(), (err) => {
+      assertEquals(err.code, "ENOENT");
+      assertEquals(err.syscall, "unlink");
+      assertEquals(err.path, path);
+    });
+});
+
+Deno.test("[std/node/fs] unlinkSync: convert Deno error to Node.js error", () => {
+  const dir = Deno.makeTempDirSync();
+  const path = join(dir, "non_existent_file");
+
+  try {
+    unlinkSync(path);
+    fail();
+  } catch (err) {
+    // deno-lint-ignore no-explicit-any
+    assertEquals((err as any).code, "ENOENT");
+    // deno-lint-ignore no-explicit-any
+    assertEquals((err as any).syscall, "unlink");
+    // deno-lint-ignore no-explicit-any
+    assertEquals((err as any).path, path);
+  }
+});