node:fs
Node.js fs module, resolvable via both ESM import and CommonJS require,
with or without the node: prefix. Elide ships its own implementation of
node:fs (it is no longer served by the embedding engine): the namespace is
defined in WebIDL, backed by a host filesystem layer built on java.nio, and
registered like the other Elide-native Node modules (node:path, node:os,
node:crypto).
The synchronous, callback, promise, stream, and watch APIs are complete.
Callback forms, fs.promises / node:fs/promises, FileHandle, AbortSignal
cancellation, file streams (createReadStream / createWriteStream), and
filesystem watching (watch / watchFile / unwatchFile) are all supported.
How it works
- The surface is declared in
protocol/webidl/node/fs.webidl; the generated baseJSFsNamespaceBaseis implemented byFsNamespace(packages/base/main/dev/elide/lang/javascript/node/fs/). - Operations marshal their arguments and call the host layer
FsPrimitives(.../lang/javascript/fs/host/), built overjava.nio.file.FilesandFileChannel. - Asynchronous forms run on
FsPrimitivesAsync(the samejava.niolayer overForkJoinPool.commonPool, returningCompletableFuture); results are delivered back to the guest on the event-loop tick, where they settle a native guest promise or invoke the callback. This works in both one-shotelide runand long-lived server modes. NodeFsModuleandNodeFsPromisesModuleregisterfsandfs/promisesinJavaScriptLang.initAllModules(), sorequire/importof either (with or without thenode:prefix) resolve through Elide's module registry.- File streams are built on the
node:streamReadable/Writableconstructors: the host drives the_read/_writehooks fromFsPrimitivesAsync. Watching uses the native backends (inotifyon Linux,FSEventson macOS,ReadDirectoryChangesWon Windows, with a JDKWatchServicefallback), installed at runtime startup; events cross from a backend daemon thread onto the event loop. - A capability sandbox (
FsPolicy) gates filesystem access for every guest language at one point:node:fsconsults it directly (it usesjava.nio, not the GraalVMFileSystem), and the polyglot guests go through a gatedElideGuestFileSystemthat consults the same policy — so JS, Python, and the JVM share one sandbox. The default policy grants everything (IOAccess.ALL), so behavior is unchanged until a sandbox is configured.
Synchronous API
- Content and descriptors:
readFileSync,writeFileSync,appendFileSync,openSync,closeSync,readSync,writeSync,fsyncSync,fdatasyncSync,ftruncateSync,truncateSync,readvSync,writevSync.readFileSync/writeFileSyncaccept either a path or a numeric file descriptor;readSyncfills aBuffer,TypedArray, orDataView. - Namespace and links:
mkdirSync(withrecursive, returning the first created path),rmSync,rmdirSync,unlinkSync,renameSync,copyFileSync,cpSync,linkSync,symlinkSync,readlinkSync,readdirSync(withwithFileTypesandrecursive),opendirSync(returns aDir),globSync(withwithFileTypesandexclude),realpathSync(plus therealpathSync.nativealias),mkdtempSync,mkdtempDisposableSync. - Metadata and access:
statSync,lstatSync,fstatSync,statfsSync(all support{ bigint: true }),existsSync,accessSync. - Permissions, ownership, times:
chmodSync/lchmodSync/fchmodSync,chownSync/lchownSync/fchownSync,utimesSync/lutimesSync/futimesSync(time values may be numbers in seconds, numeric strings, orDate). - Paths may be passed as a string, a
Buffer, or afile:URL object. fs.constantsexposes the access (F_OK/R_OK/W_OK/X_OK), copy (COPYFILE_), open (O_), file-type (S_IF), and mode (S_I) constants.
Objects
Stats/BigIntStats— theis*()predicates (isFile,isDirectory,isSymbolicLink,isBlockDevice,isCharacterDevice,isFIFO,isSocket), the numeric fields (size,mode,uid,gid, …),*Ms(and, in bigint mode,*Ns) timestamps, andDateaccessors (atime/mtime/ctime/birthtime).Dirent—name,parentPath,path, and theis*()predicates.StatFs— filesystem capacity and type (type,bsize,blocks,bfree,bavail,files,ffree).Dir—path,readSync()(returns the nextDirentornull),closeSync().
Errors
Failures surface as Node SystemErrors: a thrown Error whose message reads
: , carrying code (e.g. ENOENT,
EEXIST, EISDIR), errno (negative, as Node reports it), syscall, and —
for two-path operations — path / dest.
const fs = require("node:fs");
fs.writeFileSync("out.txt", "written by Elide\n");
const text = fs.readFileSync("out.txt", "utf8");
const st = fs.statSync("out.txt");
console.log(st.isFile(), st.size);
try {
fs.readFileSync("does-not-exist");
} catch (err) {
console.log(err.code, err.errno, err.syscall); // ENOENT -2 open
}Asynchronous API (callbacks)
Every operation with a Node callback form is supported; the last argument is a
Node-style (err, ...results) callback invoked on the event loop. The names are
the base names (no Sync suffix): readFile, writeFile, appendFile, open,
close, read, write, mkdir, rm, rmdir, unlink, rename, copyFile,
cp, link, symlink, readlink, readdir, opendir, glob, realpath,
mkdtemp, stat, lstat, fstat, statfs, access, chmod / lchmod /
fchmod, chown / lchown / fchown, utimes / lutimes / futimes,
truncate, ftruncate, fsync, fdatasync. fs.exists(path, cb) is also
provided — its callback receives a single boolean, matching Node's legacy
signature.
const fs = require("node:fs");
fs.writeFile("out.txt", "async write\n", (err) => {
if (err) throw err;
fs.readFile("out.txt", "utf8", (err, text) => {
console.log(text.trimEnd());
});
});Promise API (fs.promises / node:fs/promises)
The promise API resolves via require('node:fs/promises'),
import 'node:fs/promises', or require('fs').promises — all yield the same
namespace. Each operation returns a Promise; failures reject with the same Node
SystemError shape as the synchronous API.
import { readFile, writeFile } from "node:fs/promises";
await writeFile("out.txt", "via promises\n");
console.log((await readFile("out.txt", "utf8")).trimEnd());FileHandle
fsPromises.open(path, flags, mode) resolves to a FileHandle. It exposes
read / readv / write / writev, readFile / writeFile / appendFile,
stat, truncate, chmod, chown, utimes, sync, datasync, and close,
plus the EventEmitter surface (on / once / off / emit, emitting
close) and Symbol.asyncDispose for await using.
import { open } from "node:fs/promises";
const fh = await open("out.txt", "r");
try {
const { bytesRead, buffer } = await fh.read(Buffer.alloc(64), 0, 64, 0);
console.log(buffer.subarray(0, bytesRead).toString("utf8"));
} finally {
await fh.close();
}Cancellation (AbortSignal)
readFile, writeFile, and appendFile accept a { signal } option in both
the callback and promise forms. An already-aborted signal — or one aborted
before the work completes — rejects (or calls back) with an AbortError whose
code is ABORT_ERR.
File streams
fs.createReadStream(path[, options]) returns an fs.ReadStream (a
stream.Readable); fs.createWriteStream(path[, options]) returns an
fs.WriteStream (a stream.Writable). Both honour the usual options —
flags, encoding, fd, mode, autoClose, emitClose, start,
highWaterMark (and end for reads) — emit 'open' / 'ready' / 'close',
expose path / bytesRead (or bytesWritten) / pending, support .pipe(),
and read or write incrementally rather than buffering the whole file.
import { createReadStream, createWriteStream } from "node:fs";
createReadStream("big.log").pipe(createWriteStream("copy.log"));The FileHandle stream helpers reuse the open descriptor:
fileHandle.createReadStream(), createWriteStream(), readableWebStream() (a
WHATWG ReadableStream), and readLines() (an async iterable of lines). Unlike
the top-level streams these default to autoClose: false — the FileHandle
owns the descriptor.
Watching
fs.watch(filename[, options][, listener]) returns an fs.FSWatcher (an
EventEmitter) backed by the native OS watcher. It emits 'change' with
(eventType, filename), supports the recursive / persistent / encoding /
signal options, ref() / unref(), close(), and is async-iterable.
fsPromises.watch(filename[, options]) returns that async iterator directly.
const watcher = fs.watch("./src", { recursive: true }, (event, file) => {
console.log(event, file);
});
// later: watcher.close();fs.watchFile(filename[, options], listener) polls the file's status at an
interval and invokes the listener with the current and previous Stats;
fs.unwatchFile(filename[, listener]) stops it.
Capability sandbox
Elide can confine filesystem access with a per-path, per-operation capability policy, modeled on Deno's read/write scoping but enforced at a single host-side gate shared by every guest language:
- Each capability (read, write) is either fully granted or restricted to a set of path roots; a path is allowed only when it is at or under a granted root.
- Checks canonicalize the target (
toRealPath) before matching, so..and symlink escapes cannot evade the policy. The operation then runs against the resolved-ancestor path, so an ancestor symlink cannot be swapped between the check and the syscall (the remaining check→use window — a swap of the final component itself — needsopenat/O_NOFOLLOWand is tracked as hardening). - A denied
node:fsoperation fails with a NodeEACCESSystemError; a denied polyglot operation fails with the engine'sSecurityException. - The same
FsPolicygatesnode:fs,node:fs/promises, streams, watchers, and the polyglotFileSystem(Pythonopen, JSimport, JVM file access) — one sandbox for the whole runtime.
The default policy grants all access, matching the historical behavior; a restricted policy is opt-in.
Enabling it from the command line
The sandbox is activated by global flags. They are honored wherever Elide builds
a guest runtime — elide run, the implicit elide form, inline snippets
(-s/--snippet), and the elide mcp server — and apply to every guest
language:
--sandboxturns the sandbox on. With no grants it denies all filesystem access; pair it with--allow-read/--allow-writeto open holes.--allow-read[=PATHS]grants reads. Bare--allow-readgrants every read;--allow-read=/a,/bscopes it to those roots. Repeatable, and any occurrence implies--sandbox.--allow-write[=PATHS]grants writes, with the same bare-vs-scoped semantics.--fs-auditlogs each access decision to stderr.
# Read-only access confined to the project directory:
elide --sandbox --allow-read=. server.js
# Read anywhere, write only to a scratch dir:
elide --allow-read --allow-write=/tmp/scratch build.jsKnown differences from Node
Some details cannot be reproduced faithfully through java.nio and are
documented rather than faked:
statfsSyncreportsbsizeand the block counts from the JDKFileStore; the on-disk filesystemtypeand the inode counters (files/ffree) are reported as0.lchmodSyncthrowsENOSYS(it has no portable POSIX implementation, matching Node on Linux).stat/lstat/readdirresolve the real file type (regular, directory, symlink, block/character device, FIFO, socket) from the rawst_modeon POSIX. On platforms without theunixattribute view (e.g. Windows) special files fall back to "unknown".birthtimeis best-effort on Linux: the JDK cannot readstatx's creation time, so it mirrors the JDK'screationTime()(often the change time).ctimeuses the realunix:ctimewhere available.fs.watchuses one OS watch instance per watcher (Linuxinotifyis not multiplexed as libuv does), so a process that holds very many concurrent watchers can hit the kernel'smax_user_instanceslimit. Alwaysclose()watchers you no longer need; dropped ones are reclaimed only best-effort on GC.glob/globSyncuse a Node-style matcher:,*(globstar — a leading**/matches top-level entries too),?, character classes ([...]/[!...]), and brace alternation ({a,b}). Extended-glob groups (@(...),+(...),!(...)) are not supported.
Not yet implemented
fs.openAsBlob.
See also
- Node.js upstream:
node:osAPI Referencenode:pathAPI Reference