nodejs/node
File system
Purpose
Implement the node:fs module: callback, Promise, and synchronous variants of POSIX-style file APIs, plus higher-level pieces like createReadStream, createWriteStream, Dir, glob, cp, and the fsPromises namespace. Built on libuv's uv_fs_* request layer.
Directory layout
lib/
fs.js // public callback + sync API
fs/
promises.js // public Promise API (re-export)
internal/fs/
promises.js // fsPromises implementation
streams.js // ReadStream / WriteStream
dir.js // fs.Dir (async iterator over a directory)
glob.js // fs.glob / fsPromises.glob
cp/ // recursive copy implementation
rimraf.js // recursive remove
read_file_context.js // chunked readFile
read/ // streaming read helpers
sync_write_stream.js // SyncWriteStream (used for stdout fallback)
utils.js // shared FS helpers and option parsers
watchers.js // fs.watch / fs.watchFile
recursive_watch.js // watch-recursive on platforms without native support
observer/ // FSWatcher abstractions
src/
node_file.{cc,h} // FSReqWrap, fs.* binding
node_file-inl.h
node_file_utils.{cc,h} // shared helpers
node_dir.{cc,h} // fs.opendir
node_stat_watcher.{cc,h} // fs.watchFile poller
fs_event_wrap.cc // libuv fs_event_t wrapperKey abstractions
| Type / file | Role |
|---|---|
FSReqCallback (src/node_file.cc) |
ReqWrap-derived request used by callback APIs. |
FSReqPromise (src/node_file.cc) |
Promise variant, resolves a JS Promise when libuv finishes. |
Binding (src/node_file.cc) |
All of internalBinding('fs') — open, read, writeBuffer, stat, mkdir, etc. |
ReadStream (lib/internal/fs/streams.js) |
Readable backed by fs.read/uv_fs_read. |
WriteStream (lib/internal/fs/streams.js) |
Writable backed by fs.write/uv_fs_write. |
FileHandle (lib/internal/fs/promises.js) |
Promise-based wrapper around an open fd. |
Dir (lib/internal/fs/dir.js) |
Iterator backed by uv_fs_opendir/readdir. |
FSWatcher (lib/internal/fs/watchers.js) |
Event watcher on top of fs_event_t. |
StatWatcher (src/node_stat_watcher.cc) |
Polling-based fs.watchFile. |
How a callback fs.readFile flows
sequenceDiagram
participant JS as fs.readFile(path, cb)
participant InternalFS as lib/fs.js
participant Bind as internalBinding('fs')
participant Cpp as FSReqCallback
participant uv as libuv (uv_fs_open/read)
participant Loop as event loop
JS->>InternalFS: validate args, create context
InternalFS->>Bind: open(path, flags, mode, req)
Bind->>Cpp: schedule uv_fs_open
Cpp->>uv: uv_fs_open
uv->>Loop: dispatch to threadpool
Loop-->>Cpp: completion
Cpp-->>InternalFS: oncomplete(fd) → readNextChunk
InternalFS->>Bind: read(fd, buffer, offset, length, position, req)
Bind->>uv: uv_fs_read
uv-->>Cpp: completion
Cpp-->>InternalFS: oncomplete → maybe more reads
InternalFS-->>JS: cb(null, contents)fs.readFile does not actually read in a single uv request — it loops with chunked reads (read_file_context.js) so it can stream into a single buffer when stat is unreliable (sockets, pipes, /dev/...).
Promise vs. callback vs. sync
fs.X(...)— callback, free of allocations on the JS side beyond the request.fs.XSync(...)— synchronous; usesuv_fs_*with the loop set to do its work synchronously. Allocations are minimal but blocks the loop.fs.promises.X(...)— Promise; backed byFSReqPromisewhich resolves the V8 promise from C++. TheFileHandleAPI (fs.promises.open) is the canonical way to operate on an fd in Promise land.
Watchers
Two implementations:
fs.watch(path, opts, cb)→FSWatcheroveruv_fs_event_t(src/fs_event_wrap.cc). Recursive watch on Windows/macOS uses native; on Linux it useslib/internal/fs/recursive_watch.jsto walk subtrees.fs.watchFile(path, opts, cb)→StatWatcher, auv_timer_tplusuv_fs_stat(src/node_stat_watcher.cc). Polling, used when event watchers aren't available or aren't desired.
Streams
fs.createReadStream(path, opts) returns a ReadStream (Readable) that opens once, then loops read(fd, …) filling its buffer. fs.createWriteStream(path, opts) mirrors that with write + close. The Promise namespace exposes FileHandle.createReadStream / createWriteStream that operate on a pre-open FileHandle.
Glob
fs.glob (Node 22+) / fsPromises.glob is implemented in lib/internal/fs/glob.js using a small finite-state matcher that streams entries. Matching logic comes from deps/minimatch/. Owners: @nodejs/fs-adjacent.
Permission model
The fs permission domain (src/permission/fs_permission.cc) restricts read/write access when --permission is on. JS-side checks call process.permission.has('fs.read', path); C++ checks happen in FSReqWrap::CanAccess paths. See systems › permission-model.
Integration points
- Streams: All read/write paths plug into
lib/internal/streams/. - Buffers:
Bufferallocation is the common output path for read calls; pooled buffers come fromBuffer.allocUnsafePool. - Errors: libuv errors are wrapped via
lib/internal/errors.js#uvExceptionto produceError & { code, errno, syscall, path }. - Workers: The libuv threadpool is shared across workers; tune size with
UV_THREADPOOL_SIZE.
Entry points for modification
- Adding an FS API? JS in
lib/fs.jsandlib/fs/promises.js(or underlib/internal/fs/); C++ binding insrc/node_file.cc(Open,Read, …, registered in the binding init function); document atdoc/api/fs.md. - Adding a new error mapping? Extend
lib/internal/errors.js#uvException. - Hot-path concern? Most
fsperf wins come from avoiding extra JS↔C++ trips; check the macros insrc/node_file.cc(SyncCall,AsyncCall).
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.