Standard library / fs · on the one-page reference
fs
Stable — the
synchronous core (everything except the individually-marked functions
below) wraps long-stable POSIX syscalls (open,
read, stat, ...) that predate the runtime's newer
async I/O layer.
Mostly synchronous, statically typed and Node-conventional, plus
a small async trio (readFile/writeFile/appendFile),
file watching (watch), and file-backed streams
(createReadStream/createWriteStream). This is a
practical subset, not the full Node fs API
(a Lumen "fd" is a plain int index, not a raw OS handle)
— see the roadmap for what is next.
fs.readFileSync(path, encoding?)
(string, string?) → string — UTF-8 text; throws a catchable Error naming the path if the file can't be read.
try {
const data: string = fs.readFileSync("notes.txt");
console.log(data);
} catch (e) {
console.log(e.message); // cannot read 'notes.txt': FileNotFound
}
fs.readFile(path) Experimental Native only
string → Promise<string> — true
non-blocking I/O, no thread pool, unlike Node's
fs.promises.readFile.
async function main(): Promise<void> {
const data: string = await fs.readFile("notes.txt");
console.log(data);
}
fs.writeFile(path, data) Experimental Native only
(string, string) → Promise<void> —
the async counterpart to fs.writeFileSync; creates the file
or truncates it first, same as the sync version.
async function main(): Promise<void> {
await fs.writeFile("notes.txt", "hello async world");
console.log("done");
}
fs.appendFile(path, data) Experimental Native only
(string, string) → Promise<void> —
the async counterpart to fs.appendFileSync; creates the file
if it does not exist yet.
async function main(): Promise<void> {
await fs.appendFile("log.txt", "line one\n");
await fs.appendFile("log.txt", "line two\n");
console.log("done");
}
fs.unlink(path) Experimental Native only
string → Promise<void> —
runs on a real thread pool, unlike
readFile/writeFile/appendFile
above, so it never blocks the main thread — the same approach
Node's own async fs uses. Genuinely cross-platform, not a
Linux-only shortcut.
async function main(): Promise<void> {
await fs.unlink("stale.txt");
console.log("gone");
}
fs.mkdir(path) Experimental Native only
string → Promise<void> — thread-pool
backed, see fs.unlink above. No recursive
option this pass, matching mkdirSync's default-false
convention.
async function main(): Promise<void> {
await fs.mkdir("build");
}
fs.rmdir(path) Experimental Native only
string → Promise<void> — thread-pool
backed, see fs.unlink above.
async function main(): Promise<void> {
await fs.rmdir("build");
}
fs.stat(path) Experimental Native only
string → Promise<Stats> — thread-pool
backed, see fs.unlink above. Same Stats shape
as fs.statSync.
async function main(): Promise<void> {
const st = await fs.stat("notes.txt");
console.log(st.size);
}
fs.writeFileSync(path, data)
(string, string) → void — creates the file or truncates it first; throws a catchable Error on an I/O failure.
fs.writeFileSync("notes.txt", "hello world");
fs.appendFileSync(path, data)
(string, string) → void — creates the file if it does not exist yet.
fs.appendFileSync("log.txt", "new line\n");
fs.existsSync(path)
string → bool
if (fs.existsSync("config.json")) {
console.log("found it");
}
fs.mkdirSync(path, recursive?)
(string, bool?) → void — recursive defaults to false.
fs.mkdirSync("build/output", true);
fs.unlinkSync(path)
string → void — delete a file.
fs.unlinkSync("scratch.tmp");
fs.renameSync(oldPath, newPath)
(string, string) → void
fs.renameSync("draft.md", "final.md");
fs.copyFileSync(src, dest)
(string, string) → void
fs.copyFileSync("template.txt", "output.txt");
fs.rmdirSync(path)
string → void — removes an empty directory.
fs.rmdirSync("empty-dir");
fs.rmSync(path, recursive?)
(string, bool?) → void — recursive removes a whole directory tree.
fs.rmSync("build", true);
fs.truncateSync(path, len)
(string, int) → void
fs.truncateSync("log.txt", 0);
fs.linkSync(existingPath, newPath)
(string, string) → void — hard link.
fs.linkSync("original.txt", "alias.txt");
fs.symlinkSync(target, path)
(string, string) → void
fs.symlinkSync("/etc/hosts", "hosts-link");
fs.readlinkSync(path)
string → string
console.log(fs.readlinkSync("hosts-link")); // /etc/hosts
fs.chmodSync(path, mode)
(string, int) → void — POSIX mode bits.
fs.chmodSync("script.sh", 0o755); // rwxr-xr-x
fs.accessSync(path, mode?)
(string, int?) → bool — mode bitmask R_OK=4 W_OK=2 X_OK=1; returns
bool rather than throwing.
console.log(fs.accessSync("data.csv", 4)); // readable?
fs.cpSync(src, dest, recursive?)
(string, string, bool?) → void — falls back to a single file copy when
src is not a directory.
fs.cpSync("assets", "dist/assets", true);
fs.mkdtempSync(prefix)
string → string — unique suffix from a timestamp + counter, not cryptographic; returns the created path.
const dir: string = fs.mkdtempSync("/tmp/build-");
console.log(dir); // /tmp/build-1a2b3c4d
fs.statSync(path)
string → { size, isFile, isDirectory, mtimeMs } — the first
record-returning builtin; isFile/isDirectory are bool fields, not
methods.
const info = fs.statSync("report.pdf");
console.log(info.size);
console.log(info.isFile);
fs.openSync(path, flags)
(string, string) → int — a Lumen "fd" is an index into an internal
file table, not a raw OS handle. flags is "r" (read, must exist),
"w" (write, create/truncate), or "a" (append,
create if missing, existing content kept, every writeSync
lands at the current end of file — verified across separate
open/close cycles, each reopen correctly re-seeks to wherever the file
ends at that point, not just where it ended at the first open).
const fd: int = fs.openSync("data.bin", "r");
const logFd: int = fs.openSync("app.log", "a");
fs.writeSync(logFd, "another line\n");
fs.closeSync(logFd);
fs.closeSync(fd)
int → void
fs.closeSync(fd);
fs.readSync(fd, length)
(int, int) → string — reads up to length bytes from the
current file position; works on string, not a Buffer (Lumen has none yet).
const fd: int = fs.openSync("data.bin", "r");
const chunk: string = fs.readSync(fd, 1024);
fs.closeSync(fd);
fs.writeSync(fd, data)
(int, string) → int — writes at the current file position, returns bytes written.
const fd: int = fs.openSync("out.bin", "w");
const n: int = fs.writeSync(fd, "hello");
fs.closeSync(fd);
fs.lstatSync(path) Experimental
string → { size, isFile, isDirectory, mtimeMs } — like
statSync, but does not follow a symlink at path; stats the
link itself.
const info = fs.lstatSync("hosts-link");
console.log(info.isFile); // false: the link itself isn't a regular file
fs.fstatSync(fd) Experimental
int → { size, isFile, isDirectory, mtimeMs } — stats an
already-open fd instead of resolving a path again.
const fd: int = fs.openSync("data.bin", "r");
console.log(fs.fstatSync(fd).size);
fs.closeSync(fd);
fs.fchmodSync(fd, mode) Experimental
(int, int) → void — fd-based form of chmodSync.
const fd: int = fs.openSync("script.sh", "w");
fs.fchmodSync(fd, 0o755);
fs.closeSync(fd);
fs.lchmodSync(path, mode) Experimental
(string, int) → void — chmod without following a symlink at
path; best effort (not every OS lets you chmod a symlink directly).
fs.lchmodSync("hosts-link", 0o644);
fs.fchownSync(fd, uid, gid) Experimental
(int, int, int) → void — pass -1 for either id to leave
it unchanged.
const fd: int = fs.openSync("data.bin", "w");
fs.fchownSync(fd, 1000, -1); // change owner, leave group alone
fs.closeSync(fd);
fs.chownSync(path, uid, gid) Experimental
(string, int, int) → void — pass -1 for either id
to leave it unchanged. Follows symlinks (see fs.lchownSync
below for the one that shouldn't). Implemented by opening the file
and reusing fchownSync's working fd-based path, since the
path-based owner-change call panics unconditionally in this Zig version's
runtime.
fs.chownSync("data.bin", 1000, 1000);
fs.lchownSync(path, uid, gid) Experimental
(string, int, int) → void — like chownSync
but changes the symlink itself, not what it points to. Linux only (raw
lchown, no libc) — unlike chownSync, this
can't be built by opening the file first, since opening a symlink path
normally follows it.
fs.symlinkSync("/data/real.bin", "link.bin");
fs.lchownSync("link.bin", 1000, 1000);
fs.realpathSync(path) Experimental
string → string — resolves symlinks and returns the
canonical path. Returns path unchanged if it doesn't exist or
can't be resolved.
console.log(fs.realpathSync("./current-link"));
fs.writevSync(fd, buffers) Experimental
(int, string[]) → int — writes every chunk in
buffers in a single syscall, returns the total bytes
written. Linux only (raw writev, no libc).
const fd: int = fs.openSync("out.bin", "w");
const n: int = fs.writevSync(fd, ["hello ", "world"]);
fs.closeSync(fd);
fs.readvSync(fd, sizes) Experimental
(int, int[]) → string[] — reads into
sizes.length chunks in a single syscall, returning that many
strings sized to what was actually read. Deviation from
Node: Node's readv fills caller-provided mutable
buffers; Lumen's string is immutable, so this takes desired
chunk sizes instead and owns the buffers itself — same underlying
vectored read, a shape that fits the type system. Linux only (raw
readv, no libc).
const fd: int = fs.openSync("data.bin", "r");
const chunks: string[] = fs.readvSync(fd, [4, 100]); // header, then the rest
fs.closeSync(fd);
fs.fsyncSync(fd) Experimental
int → void — flush file data and metadata to disk.
fs.fsyncSync(fd);
fs.fdatasyncSync(fd) Experimental
int → void — aliases fsyncSync (no
data-only sync separate from a full sync is exposed here).
fs.fdatasyncSync(fd);
fs.ftruncateSync(fd, len) Experimental
(int, int) → void — fd-based form of truncateSync.
const fd: int = fs.openSync("log.txt", "w");
fs.ftruncateSync(fd, 0);
fs.closeSync(fd);
fs.futimesSync(fd, atimeMs, mtimeMs) Experimental
(int, int, int) → void — set access/modify times (milliseconds since epoch) on an open fd.
fs.futimesSync(fd, 1700000000000, 1700000000000);
fs.utimesSync(path, atimeMs, mtimeMs) Experimental
(string, int, int) → void — path-based form, follows symlinks.
fs.utimesSync("notes.txt", 1700000000000, 1700000000000);
fs.lutimesSync(path, atimeMs, mtimeMs) Experimental
(string, int, int) → void — like utimesSync, but sets
the timestamps on a symlink itself rather than its target.
fs.lutimesSync("hosts-link", 1700000000000, 1700000000000);
fs.readdirSync(path) Experimental
string → string[] — entry names in a directory, in no
particular order.
const names: string[] = fs.readdirSync(".");
for (const n of names) {
console.log(n);
}
fs.watch(path, listener) Experimental Wasm: limited
(string, (string, string) → void) → void — blocking,
never returns; calls listener with the name of the file that
changed and an event type ("change" for a data
modification, "rename" for a create/delete/move —
matching Node's own fs.watch convention, not inotify's full
granularity) on every event under path. Linux only (raw
inotify, no libc). Not EventEmitter-based like
Node's real FSWatcher — nothing drives an emitter
asynchronously on its own here, so this calls the listener directly in a
blocking loop instead, the same shape as http.createServer.
function onChange(name: string, eventType: string): void {
console.log(name + " " + eventType);
}
fs.watch("./src", onChange);
fs.createReadStream(path) Experimental
string → ReadableStream — built the
same way Map/Set/EventEmitter
are, a real dedicated type. .read() returns the next
chunk (up to 64KB), an empty string at EOF; .close()
closes the file. A missing/unopenable file degrades to a stream that
always reads "" rather than crashing, the same fallback
shape every other fs function already uses.
Synchronous/blocking, no async or backpressure integration yet.
const r = fs.createReadStream("large.log");
let chunk: string = r.read();
while (chunk != "") {
console.log(chunk.length);
chunk = r.read();
}
r.close();
fs.createWriteStream(path) Experimental
string → WritableStream —
.write(chunk) appends a chunk to the file (opens/
truncates on creation); .close() flushes and closes.
const w = fs.createWriteStream("out.log");
w.write("line one\n");
w.write("line two\n");
w.close();