This document covers Bun's implementation of Node.js-compatible file system APIs exposed through the node:fs and node:fs/promises modules. For HTTP and networking APIs, see HTTP Module. For general system bindings, see Process and System Bindings.
Bun's file system implementation provides Node.js API compatibility while leveraging native performance optimizations. The implementation spans multiple layers: JavaScript bindings, Zig native code for core operations, and cross-platform system call abstractions. Both synchronous and asynchronous operations are supported, with async operations utilizing thread pools on POSIX systems and libuv on Windows.
Sources: src/bun.js/node/node_fs.zig1-67 src/bun.js/node/node_fs_binding.zig1-75
Architecture Layer Diagram
The file system implementation consists of four distinct layers. The JavaScript layer provides the Node.js-compatible API surface through fs and fs/promises modules. The binding layer (Binding struct) marshals JavaScript values to Zig types using ArgumentsSlice for argument parsing. The implementation layer (NodeFS struct) contains the actual file system logic, dispatching to either synchronous execution or asynchronous tasks. Finally, the system call layer (sys.zig) provides cross-platform abstractions over POSIX syscalls and Windows APIs.
Sources: src/bun.js/node/node_fs.zig1-67 src/bun.js/node/node_fs_binding.zig1-215 src/js/node/fs.promises.ts1-30 src/sys.zig1-58
Binding Call Flow Diagram
The binding system uses a generic Bindings(function_name) pattern that generates type-safe wrappers for each file system operation. Each binding parses JavaScript arguments using ArgumentsSlice, converts them to Zig types via Arguments.fromJS, checks for early abort signals, creates an async task, and schedules it on the thread pool. Results are converted back to JavaScript values and resolved/rejected through promises.
Sources: src/bun.js/node/node_fs_binding.zig6-82
The Binding struct in node_fs_binding.zig provides the bridge between JavaScript and Zig code. Each file system operation has both synchronous and asynchronous variants:
| Operation Category | Sync Functions | Async Functions |
|---|---|---|
| File I/O | readFileSync, writeFileSync, appendFileSync | readFile, writeFile, appendFile |
| File Descriptors | openSync, closeSync, readSync, writeSync | open, close, read, write |
| Metadata | statSync, lstatSync, fstatSync | stat, lstat, fstat |
| Permissions | chmodSync, chownSync, fchmodSync, fchownSync | chmod, chown, fchmod, fchown |
| Directories | mkdirSync, rmdirSync, readdirSync | mkdir, rmdir, readdir |
| Links | linkSync, symlinkSync, readlinkSync, realpathSync | link, symlink, readlink, realpath |
| Advanced | copyFileSync, cpSync, rmSync | copyFile, cp, rm |
The binding layer uses compile-time code generation via the Bindings() function to create type-safe wrappers that automatically handle argument conversion, error propagation, and promise resolution.
Sources: src/bun.js/node/node_fs_binding.zig84-201
Sync vs Async Execution Paths
Synchronous operations execute directly in the calling thread, making system calls and returning results immediately. Asynchronous operations on POSIX systems use NewAsyncFSTask to create task structures that are submitted to a thread pool (WorkPool), where worker threads execute the file system operations and enqueue results back to the main event loop. On Windows, NewUVFSRequest creates libuv requests that are handled by libuv's internal thread pool, with callbacks re-entering the Bun event loop.
Sources: src/bun.js/node/node_fs.zig23-443 src/bun.js/node/node_fs_binding.zig16-82
The implementation uses two distinct async task types:
NewAsyncFSTask (POSIX and Windows for most operations):
WorkPoolTask structure with embedded argumentsNodeFS functions on thread pool workersAbortSignal for cancellationerr.clone() for thread safetyNewUVFSRequest (Windows only for specific operations):
open, close, read, write, readv, writev, statfsuv.fs_t request structuresuv_fs_open, uv_fs_read, etc.)Sources: src/bun.js/node/node_fs.zig114-334 src/bun.js/node/node_fs.zig336-442
Type System Hierarchy
The type system provides flexible input handling while maintaining type safety. PathLike accepts strings, buffers, or slices in both sync and async (threadsafe) variants. PathOrFileDescriptor allows operations to work with either paths or open file descriptors. StringOrBuffer and BlobOrStringOrBuffer provide progressively more flexible content types. The Valid namespace enforces constraints like path length limits and null byte checks.
Sources: src/bun.js/node/types.zig1-813
| Type | Purpose | Variants |
|---|---|---|
| PathLike | Flexible path input | string, buffer, slice_with_underlying_string, threadsafe_string, encoded_slice |
| PathOrFileDescriptor | Path or FD | fd: bun.FileDescriptor, path: PathLike |
| StringOrBuffer | Content input | string, threadsafe_string, encoded_slice, buffer |
| BlobOrStringOrBuffer | Extended content | blob: Blob, string_or_buffer: StringOrBuffer |
| Encoding | Text encoding | utf8, utf16le, latin1, ascii, base64, base64url, hex, buffer |
| Mode | File permissions | std.posix.mode_t (POSIX) or 0 (Windows) |
Each type provides methods for conversion (fromJS, toJS), validation, and memory management (deinit, toThreadSafe). Async operations require thread-safe variants that don't share memory with the JavaScript heap.
Sources: src/bun.js/node/types.zig135-813
File system operations define argument structures in node_fs.zig that correspond to their JavaScript signatures:
Argument Structure Examples
Each operation defines a struct with typed fields corresponding to its parameters. These structs implement fromJS() for parsing JavaScript arguments and deinit() for cleanup. Operations supporting cancellation include an optional signal: ?*jsc.AbortSignal field.
Sources: src/bun.js/node/node_fs.zig2000-3000 (approximate range, specific argument definitions)
The sys.zig module provides a unified interface over platform-specific system calls:
System Call Abstraction Layers
The sys.zig module defines a Maybe(T) result type that wraps either a successful result or an Error containing errno, syscall identifier, and optional path. Platform-specific implementations are selected at compile time, with POSIX systems using direct syscalls or libc, and Windows using kernel32/ntdll APIs or libuv wrappers.
Sources: src/sys.zig1-400
POSIX (Linux/macOS):
std.os.linux.syscall on Linuxstd.c on macOSopen() → linux.openat() or syscall.open()Windows:
\\?\ prefixCreateFileW, ReadFile, WriteFileNtQueryDirectoryFile, NtSetInformationFilesys_uv.zig for consistent async behaviorThe normalizePathWindows() function handles Windows-specific path conversions, adding NT object prefixes (\\??\) for direct ntdll usage.
Sources: src/sys.zig863-1200 src/windows.zig1-300
File Read Operation Flows
readFile performs a complete file read: opening, statting to determine size, allocating a buffer, reading in a loop to handle partial reads, and closing. read performs positioned or sequential reads into a provided buffer. readv (vectored read) reads into multiple buffers in a single syscall.
Sources: src/bun.js/node/node_fs.zig4000-5000 (approximate range)
| Function | Behavior | Flags |
|---|---|---|
| writeFile | Write entire contents, create if missing | O.WRONLY | O.CREAT | O.TRUNC |
| appendFile | Append contents, create if missing | O.WRONLY | O.CREAT | O.APPEND |
| write | Write from buffer at position | Uses provided fd |
| writev | Vectored write to multiple buffers | Uses provided fd |
Write operations handle partial writes by looping until all data is written or an error occurs. The writeFile and appendFile functions manage file lifecycle (open/close) while write and writev operate on existing file descriptors.
Sources: src/bun.js/node/node_fs.zig5000-6000 (approximate range)
| Operation | Purpose | Platform Notes |
|---|---|---|
| open | Open file, return fd | Windows: CreateFileW or uv_fs_open |
| close | Close file descriptor | Windows: Never closes stdout(1)/stderr(2) |
| fsync | Flush file data to disk | fsync() or FlushFileBuffers |
| fdatasync | Flush file data (not metadata) | fdatasync() or FlushFileBuffers |
| ftruncate | Truncate file to length | ftruncate() or SetEndOfFile |
File descriptors in Bun use bun.FileDescriptor which wraps platform-specific handles. On Windows, special handling prevents closing stdout/stderr in async close operations.
Sources: src/bun.js/node/node_fs.zig175-189
Directory Operation Decision Trees
Directory operations support both simple and recursive modes. mkdir with recursive: true creates parent directories as needed. readdir with recursive: true uses a dedicated async task to traverse subdirectories. rm supports recursive deletion of directory trees with configurable retry behavior and max retry delays.
Sources: src/bun.js/node/node_fs.zig690-750 src/sys.zig690-840
The readdir operation returns directory entries as either strings or Dirent objects:
Single-level readdir:
getdents64 syscall to read directory entriesNtQueryDirectoryFile for directory iterationDirent objects with type informationRecursive readdir:
AsyncReaddirRecursiveTaskThe Dirent class exposes file type information (isFile(), isDirectory(), isSymbolicLink(), etc.) without requiring separate stat calls.
Sources: src/bun.js/node/node_fs.zig49-75
| Function | Behavior | Follows Symlinks |
|---|---|---|
| stat | Get file metadata | Yes |
| lstat | Get file metadata | No (returns symlink info) |
| fstat | Get metadata by fd | N/A |
| statfs | Get filesystem info | N/A |
Stat operations return Stats objects with fields:
size, blocks, blksizeatimeMs, mtimeMs, ctimeMs, birthtimeMsmode, isFile(), isDirectory(), isSymbolicLink()uid, giddev, ino, rdevOn Linux, Bun uses statx syscall when available for more efficient partial stat retrieval, falling back to traditional stat/lstat/fstat if unsupported.
Sources: src/sys.zig490-681 src/bun.js/node/node_fs.zig36-43
Permission Operation Implementations
Permission operations accept mode as either numeric (e.g., 0o755) or octal string (e.g., "755"). The lchmod and lchown variants do not follow symlinks. On Windows, these operations delegate to libuv's compatibility layer due to limited Windows support for UNIX-style permissions.
Sources: src/sys.zig338-406 src/bun.js/node/node_fs.zig28-34
| Function | Accepts | Platform |
|---|---|---|
| utimes | Path + atime/mtime | All platforms |
| futimes | FD + atime/mtime | All platforms |
| lutimes | Path + atime/mtime (no follow) | All platforms |
Time values can be specified as:
The implementation converts to timespec structures with nanosecond precision on POSIX and FILETIME on Windows.
Sources: src/sys.zig683-689 src/bun.js/node/node_fs.zig39-44
Copy Operation Strategies
copyFile attempts efficient copy-on-write via clonefile() (macOS) or FICLONE ioctl (Linux), falling back to buffered read/write. cp handles recursive directory copying with options for filtering, preserving timestamps, and symlink handling. On macOS, simple recursive copies can use a single clonefile() call.
Sources: src/bun.js/node/node_fs.zig31-73 src/bun.js/node/node_fs.zig445-543
| Operation | Creates | Platform Support |
|---|---|---|
| link | Hard link | POSIX, Windows |
| symlink | Symbolic link | POSIX, Windows (requires privileges) |
| readlink | Read link target | POSIX, Windows |
| realpath | Resolve to absolute path | All platforms |
Symlink Types (Windows):
"file": File symlink"dir": Directory symlink"junction": Directory junction (no privileges required)The realpath operation has two variants:
realpathSync.native: Uses platform realpath() syscallrealpathSync: JavaScript implementation handling more edge casesSources: src/bun.js/node/node_fs.zig42-54 src/sys.zig1500-1800
The rm operation provides flexible deletion with options:
| Option | Default | Behavior |
|---|---|---|
| recursive | false | Remove directories recursively |
| force | false | Ignore nonexistent files |
| maxRetries | 0 | Retry count on failure |
| retryDelay | 100 | Initial delay between retries (ms) |
The implementation:
unlinkrecursive: trueforce: trueSources: src/bun.js/node/node_fs.zig56-61
Error Handling Flow
File system errors originate as platform errno values, combined with syscall tags and optional path information into sys.Error structures. These are converted to JavaScript Error instances with Node.js-compatible properties (code, syscall, path). The Maybe(T) wrapper type ensures errors are properly propagated through the Zig layers.
Sources: src/sys.zig302-308
| Code | Meaning | Common Causes |
|---|---|---|
| ENOENT | No such file or directory | Missing file, invalid path |
| EACCES | Permission denied | Insufficient permissions |
| EEXIST | File exists | File exists when EXCL flag set |
| EISDIR | Is a directory | Attempted file operation on directory |
| ENOTDIR | Not a directory | Attempted directory operation on file |
| EMFILE | Too many open files | Process fd limit reached |
| ENOSPC | No space left on device | Disk full |
| EROFS | Read-only file system | Write attempt on read-only filesystem |
Error objects include descriptive messages and preserve stack traces for debugging.
Sources: src/sys.zig20-34
Refresh this wiki