Skip to content

FileHandle.createWriteStream({ flush: true }) + handle.close() leaks the handle on Windows → EPERM on subsequent rename() #32829

Description

@Luligu

What version of Bun is running?

1.3.14

What platform is your computer?

Windows 11 (and Windows Server on windows-latest). Not reproducible on Linux or macOS (POSIX allows renaming an open file).

What steps can reproduce the bug?

// repro.mjs  —  run with:  bun run repro.mjs
import { open, rename, rm, mkdir, readFile } from "node:fs/promises";

const dir = "bun-rename-repro";
await rm(dir, { recursive: true, force: true });
await mkdir(dir, { recursive: true });
const final = `${dir}/file`;

for (let i = 0; i < 500; i++) {
  const tmp = `${final}.tmp`;
  const handle = await open(tmp, "w");
  const writer = handle.createWriteStream({ encoding: "utf8", flush: true });
  await new Promise((resolve, reject) => {
    writer.on("finish", resolve);
    writer.on("error", reject);
    writer.write(`v${i}`);
    writer.end();
  });
  await handle.close();        // resolves, but the OS handle is still held on Windows
  await rename(tmp, final);    // EPERM on Bun/Windows
}

console.log("done:", await readFile(final, "utf8"));

What is the expected behavior?

The loop completes and prints done: v499, exactly as on Node.js. Once await handle.close() resolves, the OS handle is released and rename() succeeds.

What do you see instead?

EPERM: operation not permitted, rename 'bun-rename-repro\file.tmp' -> 'bun-rename-repro\file'
  syscall: "rename",
  errno: -1,
  code: "EPERM"

It typically fails on the very first iteration.

Additional information

  • The defect is specific to the write‑stream path. The following alternatives all work on Bun/Windows, so rename() itself is fine — only the createWriteStream(...) + close() handle lifecycle is affected:
    • await writeFile(tmp, value) + rename
    • await handle.writeFile(value) + close + rename
    • await Bun.write(tmp, value) + rename
  • Retrying the rename does not help. A retry/backoff (10 attempts, ~275 ms total) still throws EPERM, which suggests the handle is not released within any usable window rather than losing a transient race.
  • Real‑world impact: this is the standard atomic‑write used by storage layers. We hit it via matter.js (@matter/nodejs FileStorageDriver.#writeAndMoveFile), which breaks all persistent storage when running the app on Bun for Windows.

Thanks again for looking into it — happy to provide more details, traces, or test against a patched build.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions