This document covers Bun's implementation of Node.js-compatible file system APIs exposed through the node:fs and node:fs/promises modules. It details the multi-layered architecture from JavaScript bindings down to platform-specific system call abstractions.
Bun's file system implementation provides a high-performance, Node.js-compatible API surface. The implementation spans JavaScript (for API ergonomics and shim logic), Rust (for core FS logic and async task management), and platform-specific native code. Asynchronous operations are dispatched through a work pool on POSIX systems, and through libuv on Windows via bun_sys::sys_uv.
Sources: src/js/node/fs.ts1-18 src/js/node/fs.promises.ts1-8 src/runtime/node/node_fs.rs1-30 src/sys/lib.rs1-40
The implementation is structured into four layers, bridging high-level JavaScript calls to low-level kernel operations.
Module and Binding Architecture
Sources: src/js/node/fs.ts1-18 src/js/node/fs.promises.ts1-10 src/js/internal/fs/binding.ts1-5 src/runtime/node/node_fs.rs1-30 src/sys/lib.rs1-40 src/sys/sys_uv.rs1-20
The four layers:
node:fs/promises uses asyncWrap to bridge to native bindings src/js/node/fs.promises.ts131-165 The node:fs module provides callback-based APIs by wrapping the same underlying promise calls src/js/node/fs.ts48-120 Streams, watch, and watchFile are lazily loaded sub-modules.src/js/internal/fs/binding.ts loads the native NodeJSFS Rust binding. All FS methods on the native object are called as fs.<method>(...) throughout the JS layer src/js/node/fs.promises.ts4src/runtime/node/node_fs.rs contains the core sync and async FS logic. PathLike, StringOrBuffer, and related conversion types live in src/runtime/node/types.rs. FSWatcher is in src/runtime/node/node_fs_watcher.rs.bun_sys (src/sys/lib.rs) provides cross-platform syscall wrappers. On Windows, most FS syscalls route through src/sys/sys_uv.rs, which wraps libuv's uv_fs_* family.The binding system connects the JavaScript layer to the Rust NodeJSFS implementation.
fs.promises Call Flow
Sources: src/js/node/fs.promises.ts127-174 src/runtime/node/node_fs.rs1-50 src/sys/lib.rs1-40
Key components:
asyncWrap: Wraps native sync-like Rust functions as JS Promises. Every export in fs.promises.ts that simply forwards to fs.<method> uses this utility src/js/node/fs.promises.ts225-232FileHandle unwrapping: readFile, writeFile, and appendFile check whether the first argument is a FileHandle instance and extract the raw kFd fd number before calling the native binding src/js/node/fs.promises.ts133-137 src/js/node/fs.promises.ts170-174node:fs: Functions in fs.ts call into the same native binding but wrap the result in Node.js-style (err, result) callbacks. The nullcallback micro-optimization avoids allocating a new closure for each call src/js/node/fs.ts39-42validateFunction, validateInteger, and getValidatedPath from internal/validators guard all public entry points src/js/internal/validators.ts77-115Sync functions on the node:fs module (readFileSync, writeFileSync, statSync, etc.) are bound directly from the native NodeJSFS object using .bind(fs) src/js/node/fs.ts423-481 They execute inline on the JS thread without dispatching a work pool task.
realpath on WindowsrealpathSync and realpath have a custom JavaScript implementation on Windows to match Node.js behavior around drive substitution and junction resolution. The POSIX path delegates directly to the native Rust binding src/js/node/fs.ts575-845
FileHandle is defined in src/js/node/fs.promises.ts and is the promise-based equivalent of a raw file descriptor. It is returned by fs.promises.open() and supports await using via Symbol.asyncDispose.
FileHandle internals
| Private Symbol | Purpose |
|---|---|
kFd | Stores the numeric file descriptor; -1 when closed |
kRefs | Reference count; fd is closed when it reaches 0 |
kClosePromise | In-flight close promise, deduplicated |
kCloseResolve / kCloseReject | Resolve/reject handles for deferred close |
kFlag | Open flags string ("r", "w", etc.) |
kRef / kUnref | Increment/decrement kRefs around async ops |
Sources: src/js/node/fs.promises.ts14-26 src/js/node/fs.promises.ts256-310
Key FileHandle methods:
| Method | Notes |
|---|---|
read(buffer, offset, length, position) | Reads into an existing buffer; returns { bytesRead, buffer } |
readv(buffers, position) | Scatter read into multiple buffers |
write(buffer, offset, length, position) | Returns { bytesWritten, buffer } |
writev(buffers, position) | Gather write from multiple buffers |
readFile(options) | Reads entire file; returns Buffer or string |
writeFile(data, options) | Writes/truncates file using the open fd |
stat(options) | Returns Stats via fstat |
truncate(len) | Truncates via ftruncate |
datasync() / sync() | fdatasync / fsync |
readableWebStream() | Returns Bun.file(fd).stream() |
createReadStream(options) | Returns an internal/fs/streams ReadStream |
createWriteStream(options) | Returns an internal/fs/streams WriteStream |
readLines(options) | Returns a readline.Interface over createReadStream |
close() | Decrements kRefs; closes fd when refs hit 0 |
<FileRef file-url="https://github.com/oven-sh/bun/blob/a0e221e0/Symbol.asyncDispose" undefined file-path="Symbol.asyncDispose">Hii</FileRef> | Alias for close(), enabling await using |
Sources: src/js/node/fs.promises.ts280-608
fs.createReadStream() and fs.createWriteStream() (and their FileHandle equivalents) are implemented in src/js/internal/fs/streams.ts as subclasses of node:stream's Readable and Writable.
ReadStream is created with a plain path string and piped directly to a response or other sink, Bun can bypass the stream machinery entirely by using Bun.file(path) src/js/internal/fs/streams.ts42-45WriteStream created from a plain path uses Bun.file(path).writer() (FileSink) to avoid the Node.js stream overhead src/js/internal/fs/streams.ts45-46FileHandle integration: When a FileHandle is the underlying fd, fileHandleStreamFs bridges FileHandle methods to the fs.read/fs.write callbacks the stream expects src/js/internal/fs/streams.ts55-85Sources: src/js/internal/fs/streams.ts1-86
Bun provides Node.js-compatible fs.watch() and fs.watchFile() implementations.
fs.watch() — FSWatcherThe JS FSWatcher wrapper is lazily loaded from src/js/internal/fs/watch.ts. The native implementation is the FSWatcher struct in src/runtime/node/node_fs_watcher.rs.
FSWatcher component map
Sources: src/js/internal/fs/watch.ts1-30 src/runtime/node/node_fs_watcher.rs1-70
Key FSWatcher properties in src/runtime/node/node_fs_watcher.rs:
path_watcher: Points to a platform-specific PathWatcher (inotify/FSEvents/kqueue on POSIX, ReadDirectoryChangesW on Windows).persistent / poll_ref (KeepAlive): Controls whether the watcher keeps the event loop alive.signal (AbortSignalRef): Supports AbortSignal-based cancellation.encoding: Determines how filenames are encoded in events (e.g., "utf8", "buffer").pending_activity_count (AtomicU32): Tracked to determine GC liveness via has_pending_activity().closed (Cell<bool>): Prevents double-close races.Sources: src/runtime/node/node_fs_watcher.rs44-70
fs.watchFile() — StatWatcherfs.watchFile() and fs.unwatchFile() are lazily loaded from src/js/internal/fs/watchfile.ts. The implementation uses polling: a native StatWatcher handle periodically calls stat() on the target file and emits change events when curr and prev stats differ.
watchFile() on the same path (including via file: URL) share a single StatWatcher instance.unwatchFile(path, listener) removes one listener; unwatchFile(path) removes all and stops polling.bigint: true option causes Stats timestamps to be reported as BigInt values.Sources: src/js/internal/fs/watchfile.ts1-30 test/js/node/watch/fs.watchFile.test.ts1-50
fs.cp / fs.cpSync)Both cp (async) and cpSync (sync) are implemented in src/js/node/fs.promises.ts and src/js/node/fs.ts respectively.
fs.cp / fs.cpSync Rust binding src/js/node/fs.promises.ts105-113clonefile(): On macOS, the native path can perform a recursive clone in a single clonefile() syscall.dereference, filter, preserveTimestamps, or verbatimSymlinks options are set, Bun uses a ported JavaScript implementation (internal/fs/cp / internal/fs/cp-sync) for full Node.js compatibility src/js/node/fs.promises.ts110-114fs.readdir)readdir supports three modes:
string[]{ withFileTypes: true }: returns Dirent[] with name, path/parentPath, and type methods{ recursive: true }: triggers native recursive directory scanning in RustDirent instances and Stats instances are both provided as native classes from the NodeJSFS binding src/js/node/fs.ts504-505
openAsBlob)fs.openAsBlob(path, options) wraps Bun.file(path, options), returning a Blob-compatible object that can be streamed into fetch, Response, or ReadableStream src/js/node/fs.ts44-46
Dir and opendirDir is a JavaScript class implemented in src/js/node/fs.ts that supports iterating over directory entries one at a time via readSync() / read() / close() and the async iterator protocol. It holds a handle internally and calls into the native binding for each batch of entries src/js/node/fs.ts897-980
Sources: src/js/node/fs.promises.ts105-114 src/js/node/fs.ts44-46 src/js/node/fs.ts897-980
Bun validates path lengths before dispatching syscalls to prevent buffer overflows.
| Platform | Limit | Constant | Error |
|---|---|---|---|
| POSIX | 4096 UTF-8 bytes | MAX_PATH_BYTES | ENAMETOOLONG |
| Windows (UTF-16) | 32767 u16 code units | PATH_MAX_WIDE | ENAMETOOLONG |
| Windows (UTF-8 input) | ~98302 bytes | PathBuffer size | ENAMETOOLONG |
Path length is measured in UTF-8 bytes, not JavaScript character count (UTF-16 code units). A 2000-character CJK string (2000 UTF-16 units) encodes to 6000 UTF-8 bytes and correctly triggers ENAMETOOLONG on POSIX test/js/node/fs/fs-path-length.test.ts13-18
On Windows, normalizePathWindows transcodes UTF-8 paths to UTF-16 into fixed [32767]u16 wide buffers. Paths that exceed this width after conversion (including the NT prefix added for long-path support) are rejected before any kernel call test/js/node/fs/fs-path-length.test.ts60-107
Sources: test/js/node/fs/fs-path-length.test.ts1-107 src/runtime/node/types.rs1-12
Bun uses specialized task types for file system operations to integrate with the event loop.
| Entity | Role | File Reference |
|---|---|---|
AsyncFSTask | Core Zig task for executing file system operations in a background thread. | node_fs.zig |
UVFSRequest | Windows-specific request object for libuv file system operations. | node_fs.zig |
asyncWrap | JS utility to wrap native async bindings into Promises. | src/js/node/fs.promises.ts133 |
FileHandle | Node.js compatible object wrapping a file descriptor (kFd). | src/js/node/fs.promises.ts166 |
Bun ensures that asynchronous file system errors include meaningful stack traces, even when crossing the native/JS boundary. This is achieved by capturing the stack during the initial JS call and attaching it to the resulting error if the operation fails test/js/node/fs/promises.test.js206-229
Sources: src/js/node/fs.promises.ts133-165 test/js/node/fs/promises.test.js206-230
Refresh this wiki
This wiki was recently refreshed. Please wait 1 day to refresh again.