Bun 10 ⚡ File System Operations and Streams with Bun’s Native APIs
Bun’s file system layer is not a wrapper around Node’s fs module. It is a set of native APIs—Bun.file() and Bun.write()—implemented in Zig and optimized for the runtime’s event loop. The BunFile object is a lazy reference: creating it does not read the file from disk. The data is loaded only when a method is called, and the method determines the format: text, JSON, bytes, or a stream . For operations that the native APIs do not cover—mkdir, readdir, stat—Bun provides a nearly complete implementation of node:fs, so existing code continues to work without modification .
Key point: Bun.file() creates a lazy reference. Bun.write() writes data to disk and returns the number of bytes written. The BunFile.stream() method returns a ReadableStream for processing large files without loading them into memory. The native APIs are the recommended path; node:fs is the fallback for operations not yet covered.
Why native file APIs exist
The lazy reference problem. Reading a file into memory is unnecessary when only the size or MIME type is needed. Bun.file() creates a BunFile object that represents the file but does not read it. The size and type properties are available immediately. The contents are loaded only when text(), json(), bytes(), or arrayBuffer() is called . This is the difference between asking “how large is this file?” and “give me the entire contents.”
The format problem. A file is just bytes. The application needs text, or JSON, or a Uint8Array. BunFile exposes a method for each format. The conversion happens at the point of use, not at the point of reference. This is the same pattern as the Fetch API’s Response object, and BunFile conforms to the Blob interface for compatibility .
The write problem. Bun.write() accepts a destination and a data source. The source can be a string, a Blob, an ArrayBuffer, a TypedArray, or even a Response object. Writing a fetched response to disk is a single call. Writing a file to another file is a single call. The method returns the number of bytes written, which is useful for verification .
The large-file problem. Reading a multi-gigabyte file into memory is not viable. The stream() method returns a ReadableStream that yields chunks. The chunks can be processed one at a time with for await, piped to a WritableStream, or passed to Bun.write() as the source. The memory footprint is bounded by the chunk size, not the file size .
The trade-off. The native APIs are optimized for Bun, but they are not a complete replacement for node:fs. Directory operations, file watching, and symbolic link handling still require node:fs. The native APIs cover the common cases—reading, writing, streaming, deleting—and the Node compatibility layer covers the rest .
a. Reading files with Bun.file()
The Bun.file() function accepts a path, a file descriptor, or a URL. The returned BunFile object has properties and methods for inspecting and reading the file.
// Create a lazy reference
const file = Bun.file("./data.json");
// Inspect without reading
console.log(file.size); // bytes, 0 if missing
console.log(file.type); // "application/json;charset=utf-8"
// Read in the desired format
const text = await file.text();
const json = await file.json();
const bytes = await file.bytes();
const buffer = await file.arrayBuffer();
The exists() method checks whether the file is present. A BunFile can reference a path that does not exist; no error is thrown until a read method is called .
const missing = Bun.file("./notreal.txt");
console.log(await missing.exists()); // false
The default MIME type is text/plain;charset=utf-8. It can be overridden with the second argument:
const config = Bun.file("./config", { type: "application/json" });
console.log(config.type); // "application/json;charset=utf-8"
The slice() method creates a view into the file without reading it. This is useful for reading headers or specific byte ranges from large files .
const header = Bun.file("./large.bin").slice(0, 1024);
const headerBytes = await header.arrayBuffer();
b. Writing files with Bun.write() and FileSink
Bun.write() writes data to a destination. The destination can be a path string, a BunFile, or a file descriptor. The data can be a string, a Blob, a TypedArray, an ArrayBuffer, or a Response .
// Write a string
const bytes = await Bun.write("./output.txt", "Hello, Bun");
console.log(bytes); // 10
// Write a fetched response
const response = await fetch("https://example.com");
await Bun.write("./page.html", response);
// Copy a file
await Bun.write("./copy.txt", Bun.file("./source.txt"));
// Write with options
await Bun.write("./secret.txt", data, {
mode: 0o600,
createPath: true,
});
The FileSink provides a writer for incremental writes. It is created with file.writer() and used with write() and end(). The highWaterMark option controls the buffer size before flushing to disk .
const writer = Bun.file("./log.txt").writer();
writer.write("Line 1\n");
writer.write("Line 2\n");
await writer.end();
The pipeTo() method connects a ReadableStream to a FileSink, which is the correct pattern for streaming a large file from one location to another without loading it into memory .
await Bun.file("./input.txt").stream().pipeTo(Bun.file("./output.txt").writer());
c. Streams: ReadableStream and the direct mode
The BunFile.stream() method returns a ReadableStream. The stream can be consumed with for await, a reader, or pipeTo(). Bun implements the standard Web Streams API, and also provides a “direct” mode that avoids the internal queue .
// Standard stream: chunks are enqueued
const stream = Bun.file("./large.txt").stream();
for await (const chunk of stream) {
console.log(chunk); // Uint8Array
}
The direct ReadableStream is created with type: "direct". The controller uses write() instead of enqueue(). The destination handles the queuing, and backpressure is managed through the return value of write(): a number for success, a Promise<number> when the buffer is full, or 0 when the destination has disconnected .
const direct = new ReadableStream({
type: "direct",
async pull(controller) {
for (const chunk of chunks) {
await controller.write(chunk);
}
controller.close();
},
});
The direct stream is useful for Bun.serve() responses, Bun.write(), and fetch() bodies. In these cases, the destination consumes the entire body, and the queueing that a standard ReadableStream performs is unnecessary overhead .
The highWaterMark option controls how many bytes are buffered before backpressure is applied. The default is 64 KiB. It is passed as the second argument to the ReadableStream constructor .
Complete Example Session
# ============================================
# PART 1: LAZY REFERENCE
# ============================================
const file = Bun.file("./data.json");
console.log(file.size); // does not read the file
console.log(file.type); // "application/json;charset=utf-8"
# ============================================
# PART 2: READ AS TEXT
# ============================================
const text = await file.text();
console.log(text);
# ============================================
# PART 3: READ AS JSON
# ============================================
const json = await file.json();
console.log(json.key);
# ============================================
# PART 4: WRITE A STRING
# ============================================
const bytes = await Bun.write("./output.txt", "Hello");
console.log(bytes); // 5
# ============================================
# PART 5: WRITE A RESPONSE
# ============================================
const response = await fetch("https://bun.sh");
await Bun.write("./bun.html", response);
# ============================================
# PART 6: COPY A FILE
# ============================================
await Bun.write("./copy.txt", Bun.file("./source.txt"));
# ============================================
# PART 7: STREAM A FILE
# ============================================
const stream = Bun.file("./large.log").stream();
for await (const chunk of stream) {
console.log(chunk.length);
}
# ============================================
# PART 8: FILE SINK
# ============================================
const writer = Bun.file("./log.txt").writer();
writer.write("Entry 1\n");
writer.write("Entry 2\n");
await writer.end();
# ============================================
# PART 9: PIPE STREAM TO FILE
# ============================================
await Bun.file("./input.txt").stream().pipeTo(Bun.file("./output.txt").writer());
The nine parts trace the file I/O lifecycle: lazy reference, text read, JSON read, string write, response write, file copy, stream read, file sink, and stream piping.
Quick Reference
Bun.file() Methods
| Method | Returns |
|---|---|
.text() | Promise<string> |
.json() | Promise<any> |
.bytes() | Promise<Uint8Array> |
.arrayBuffer() | Promise<ArrayBuffer> |
.stream() | ReadableStream |
.exists() | Promise<boolean> |
.delete() | Promise<void> |
.writer() | FileSink |
.slice(start, end) | Blob (lazy view) |
Bun.write() Options
| Option | Purpose |
|---|---|
mode | File permissions (octal) |
createPath | Create parent directories |
Stream Types
| Type | Constructor | Controller |
|---|---|---|
| Standard | new ReadableStream({ start }) | enqueue() |
| Direct | new ReadableStream({ type: "direct", pull }) | write() |
Best Practices
✅ Do This:
const file = Bun.file("./data.json");
const json = await file.json();
<!-- Use the specific format method --> # ✅
await Bun.file("./input.txt").stream().pipeTo(Bun.file("./output.txt").writer());
<!-- Pipe streams for large files --> # ✅
const writer = Bun.file("./log.txt").writer();
await writer.end();
<!-- Always end the FileSink --> # ✅
❌ Don’t Do This:
# Read a file into memory just to check its size
<!-- Use file.size instead --> # ❌
# Use readFileSync for large files
<!-- Use stream() for bounded memory --> # ❌
# Forget to await Bun.write()
<!-- The write is asynchronous --> # ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
size is 0 for missing file | Lazy reference | await file.exists() |
Bun.write returns 0 | Invalid destination | Verify path and permissions |
| Stream not closed | Missing end() or close() | Call writer.end() or controller.close() |
| Memory grows with large file | Using .text() instead of .stream() | Use stream() and iterate |
| MIME type incorrect | Default is text/plain | Pass { type: "..." } |
Real-World Examples
1. Read JSON
const config = await Bun.file("./config.json").json();
2. Write String
await Bun.write("./output.txt", "Hello");
3. Copy File
await Bun.write("./copy.txt", Bun.file("./source.txt"));
4. Write Response
await Bun.write("./page.html", await fetch("https://example.com"));
5. Stream Large File
for await (const chunk of Bun.file("./large.log").stream()) { ... }
6. File Sink
const writer = Bun.file("./log.txt").writer();
writer.write("Line\n");
await writer.end();
7. Pipe Stream
await Bun.file("./in.txt").stream().pipeTo(Bun.file("./out.txt").writer());
8. Check Existence
if (await Bun.file("./data.json").exists()) { ... }
9. Delete File
await Bun.file("./old.txt").delete();
10. Custom MIME Type
Bun.file("./data", { type: "application/json" });
Visual
BunFile Lifecycle
┌──────────────────────────────────────────────┐
│ Bun.file("./data.json") │
│ → Lazy reference created │
│ → No disk read │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ .size / .type │
│ → Metadata read (minimal I/O) │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ .text() / .json() / .bytes() │
│ → Full file read into memory │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ .stream() │
│ → Chunked read (bounded memory) │
└──────────────────────────────────────────────┘
Stream Piping
┌──────────────────────────────────────────────┐
│ Bun.file("./input.txt").stream() │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ ReadableStream │
│ → Chunks yielded one at a time │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ .pipeTo(Bun.file("./output.txt").writer()) │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ FileSink writes chunks to disk │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Lazy reference | Bun.file(path) |
| Text read | .text() |
| JSON read | .json() |
| Bytes read | .bytes() |
| Stream read | .stream() |
| Write | Bun.write(dest, data) |
| Incremental write | file.writer() → FileSink |
| Delete | .delete() |
| Stream piping | stream().pipeTo(writer()) |
Key takeaways:
Bun.file()is lazy. Creating aBunFiledoes not read the file. Thesizeandtypeproperties are available immediately; the contents are loaded only when a read method is called .Bun.write()is the single write API. It accepts strings,Blobs,ArrayBuffers,TypedArrays, andResponseobjects. The return value is the number of bytes written .stream()is for large files. It returns aReadableStreamthat yields chunks. The memory footprint is bounded by the chunk size, not the file size .FileSinkis for incremental writes. Thewriter()method returns a sink that buffers writes and flushes to disk onend(). ThehighWaterMarkoption controls the buffer size .node:fsis the fallback. Operations not covered by the native APIs—mkdir,readdir,stat—are available through Bun’s near-complete implementation of the Node module .
Remember: Bun’s file APIs are built for the runtime. Bun.file() is a reference, not a read. Bun.write() is the write. stream() is the escape hatch for files too large to fit in memory. The native APIs cover the common cases; node:fs covers the rest. The reference is lazy, the write is explicit, and the stream is bounded.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!