FileSystem Platform Abstraction
Use effect FileSystem for platform-abstract file I/O. Stock layers are provided for Node.js and Bun; @effect/platform-browser does not provide a FileSystem layer, so browser code needs a custom/injected implementation.
Basic Pattern
import { FileSystem } from 'effect';
import { Effect } from 'effect';
// Service injection via yield*
const program = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
// Use fs methods here
const content = yield* fs.readFileString('path/to/file.txt');
return content;
});
Reading Operations
Read File (Binary)
import { FileSystem } from 'effect';
import { Effect } from 'effect';
const readBinary = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const bytes = yield* fs.readFile('data.bin');
return bytes; // Uint8Array
});
Read File (String)
import { FileSystem } from 'effect';
import { Effect } from 'effect';
const readText = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const content = yield* fs.readFileString('config.json');
return content; // string
});
Stream File
import { FileSystem } from 'effect';
import { Effect } from 'effect';
const streamFile = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
// fs.stream() returns a Stream directly — not an Effect
const stream = fs.stream('large-file.log');
return stream; // Stream<Uint8Array, PlatformError>
});
Read Directory
import { FileSystem } from 'effect';
import { Effect } from 'effect';
const listFiles = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const entries = yield* fs.readDirectory('src/');
return entries; // ReadonlyArray<string>
});
Read Symbolic Link
import { FileSystem } from 'effect';
import { Effect } from 'effect';
const readLink = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const target = yield* fs.readLink('symlink');
return target; // string
});
Writing Operations
Write File (Binary)
import { FileSystem } from 'effect';
import { Effect } from 'effect';
const writeBinary = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const data = new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f]);
yield* fs.writeFile('output.bin', data);
});
Write File (String)
import { FileSystem } from 'effect';
import { Effect } from 'effect';
const writeText = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
yield* fs.writeFileString('output.txt', 'Hello, World!');
});
Sink (Stream Writing)
import { FileSystem } from 'effect';
import { Effect, Stream, pipe } from 'effect';
const writeStream = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
// fs.sink() returns a Sink directly — not an Effect
const sink = fs.sink('output.log');
yield* pipe(
Stream.fromIterable(['line 1\n', 'line 2\n', 'line 3\n']),
Stream.mapEffect((s) => Effect.succeed(new TextEncoder().encode(s))),
Stream.run(sink)
);
});
File Operations
Copy File
import { FileSystem } from 'effect';
import { Effect } from 'effect';
const copyFile = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
yield* fs.copyFile('source.txt', 'dest.txt');
});
Copy (Recursive Directory)
import { FileSystem } from 'effect';
import { Effect } from 'effect';
const copyDir = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
yield* fs.copy('src-dir/', 'dest-dir/');
});
Rename/Move
import { FileSystem } from 'effect';
import { Effect } from 'effect';
const renameFile = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
yield* fs.rename('old-name.txt', 'new-name.txt');
});
Remove
import { FileSystem } from 'effect';
import { Effect } from 'effect';
const removeFile = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
yield* fs.remove('file.txt');
});
// Remove directory recursively
const removeDir = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
yield* fs.remove('directory/', { recursive: true });
});
Open File Handle
import { FileSystem } from 'effect';
import { Effect } from 'effect';
const useFileHandle = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
// File handles are scoped — automatically closed when scope exits
yield* Effect.scoped(
Effect.gen(function* () {
const file = yield* fs.open('data.txt', { flag: 'r' });
// File methods return branded Size values for byte counts and offsets.
const buffer = new Uint8Array(1024);
const bytesRead = yield* file.read(buffer);
const offset = yield* file.seek(FileSystem.Size(0), 'start');
})
);
});
file.seek(offset, from) accepts a SizeInput, supports from: 'start' | 'current', and returns the new offset as FileSystem.Size (not void or a plain number). Open File handles no longer expose a descriptor property or File.Descriptor type as of beta.103; use the scoped handle operations (read, readAlloc, write, writeAll, seek, stat, sync, and truncate) instead.
Directory Operations
Make Directory
import { FileSystem } from 'effect';
import { Effect } from 'effect';
const createDir = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
yield* fs.makeDirectory('new-dir/');
// Recursive directory creation
yield* fs.makeDirectory('path/to/nested/dir/', { recursive: true });
});
Make Temp Directory
import { FileSystem } from 'effect';
import { Effect } from 'effect';
const useTempDir = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const tempPath = yield* fs.makeTempDirectory();
// Use tempPath
yield* fs.writeFileString(`${tempPath}/temp-file.txt`, 'data');
// Manual cleanup required
yield* fs.remove(tempPath, { recursive: true });
});
Make Temp Directory (Scoped)
import { FileSystem } from 'effect';
import { Effect } from 'effect';
const useScopedTempDir = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const tempPath = yield* fs.makeTempDirectoryScoped();
// Use tempPath within scope
yield* fs.writeFileString(`${tempPath}/temp-file.txt`, 'data');
// Automatically cleaned up when scope exits
}).pipe(Effect.scoped);
Metadata Operations
Stat (File Info)
import { FileSystem } from 'effect';
import { Effect, Console } from 'effect';
const getFileInfo = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const info = yield* fs.stat('file.txt');
yield* Console.log(`Type: ${info.type}`);
// "File" | "Directory" | "SymbolicLink" | "BlockDevice" | "CharacterDevice" | "FIFO" | "Socket" | "Unknown"
yield* Console.log(`Size: ${info.size}`); // FileSystem.Size (branded bigint)
yield* Console.log(`Modified: ${info.mtime}`); // Option<Date>
yield* Console.log(`Accessed: ${info.atime}`); // Option<Date>
yield* Console.log(`Created: ${info.birthtime}`); // Option<Date>
});
Access (Check Permissions)
import { FileSystem } from 'effect';
import { Effect } from 'effect';
const checkAccess = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
// Check if file exists and is readable
yield* fs.access('file.txt', { readable: true });
// Check writable
yield* fs.access('file.txt', { writable: true });
// Check if file exists (ok)
yield* fs.access('script.sh', { ok: true });
});
Exists
import { FileSystem } from 'effect';
import { Effect } from 'effect';
const fileExists = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const exists = yield* fs.exists('file.txt');
return exists; // boolean
});
Real Path (Resolve Symlinks)
import { FileSystem } from 'effect';
import { Effect } from 'effect';
const resolvePath = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const realPath = yield* fs.realPath('symlink-or-relative-path');
return realPath; // string (absolute path)
});
Permission Operations
Change Mode (chmod)
import { FileSystem } from 'effect';
import { Effect } from 'effect';
const changeMode = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
yield* fs.chmod('script.sh', 0o755);
});
Change Owner (chown)
import { FileSystem } from 'effect';
import { Effect } from 'effect';
const changeOwner = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
yield* fs.chown('file.txt', 1000, 1000); // uid, gid
});
Update Times (utimes)
import { FileSystem } from 'effect';
import { Effect } from 'effect';
const updateTimes = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const now = new Date();
yield* fs.utimes('file.txt', now, now); // atime, mtime
});
Links
Hard Link
import { FileSystem } from 'effect';
import { Effect } from 'effect';
const createHardLink = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
yield* fs.link('original.txt', 'hardlink.txt');
});
Symbolic Link
import { FileSystem } from 'effect';
import { Effect } from 'effect';
const createSymlink = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
yield* fs.symlink('target.txt', 'symlink.txt');
});
Watching
Watch Files/Directories
import { FileSystem } from 'effect';
import { Effect, Stream, Console, pipe } from 'effect';
const watchFiles = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
// fs.watch() returns a Stream directly — not an Effect
const events = fs.watch('src/'); // direct children only
return events; // Stream<WatchEvent, PlatformError>
});
// Consume watch events
const consumeWatchEvents = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const events = fs.watch('config/', { recursive: true });
yield* pipe(
events,
Stream.runForEach((event) =>
Console.log(`Event: ${event._tag}, Path: ${event.path}`)
)
);
});
Directory watching is non-recursive by default. Pass { recursive: true } to include changes in nested subdirectories. Watching a file watches that file; the recursive option matters for directory trees.
Size Helpers
import { FileSystem } from 'effect';
import { Effect } from 'effect';
const { Size, KiB, MiB, GiB, TiB, PiB } = FileSystem;
// Create size values
const
const tenKb = KiB(10);
const
const fiveGb = GiB(5);
const
const
// Use with file operations
const checkFileSize = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const info = yield* fs.stat('large-file.bin');
const maxSize = MiB(100);
if (info.size > maxSize) {
yield* Effect.fail(new Error('File too large'));
}
});
Error Handling
SystemErrorTag Values
FileSystem operations fail with PlatformError containing a SystemErrorTag:
AlreadyExists- File/directory already existsBadResource- Invalid file descriptor or handleBusy- Resource is busyInvalidData- Invalid data formatNotFound- File/directory not foundPermissionDenied- Insufficient permissionsTimedOut- Operation timed outUnexpectedEof- Unexpected end of fileUnknown- Unknown errorWouldBlock- Operation would blockWriteZero- Write operation wrote zero bytes
Error Handling Pattern
import { FileSystem } from 'effect';
import { Effect, pipe } from 'effect';
const readConfigWithFallback = pipe(
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
return yield* fs.readFileString('config.json');
}),
Effect.catchTag('PlatformError', (error) => {
if (error.reason._tag === 'NotFound') {
return Effect.succeed('{}');
}
if (error.reason._tag === 'PermissionDenied') {
return Effect.fail(
new Error('Cannot read config: permission denied')
);
}
return Effect.fail(error);
})
);
Typed Error Recovery
import { FileSystem } from 'effect';
import { Effect, Schema, pipe } from 'effect';
class ConfigNotFound extends Schema.TaggedError<ConfigNotFound>()(
'ConfigNotFound',
{
path: Schema.String
}
) {}
class ConfigInvalid extends Schema.TaggedError<ConfigInvalid>()(
'ConfigInvalid',
{
path: Schema.String,
reason: Schema.String
}
) {}
const readConfig = (path: string) =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const content = yield* pipe(
fs.readFileString(path),
Effect.mapError((error) =>
error.reason._tag === 'NotFound'
? new ConfigNotFound({ path })
: new ConfigInvalid({ path, reason: error.message })
)
);
return content;
});
Scoped Resources Pattern
import { FileSystem } from 'effect';
import { Effect } from 'effect';
const processInTempDir = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
// Create temp directory with automatic cleanup
const tempDir = yield* fs.makeTempDirectoryScoped();
// Do work in temp directory
const inputPath = `${tempDir}/input.txt`;
const outputPath = `${tempDir}/output.txt`;
yield* fs.writeFileString(inputPath, 'data');
const content = yield* fs.readFileString(inputPath);
yield* fs.writeFileString(outputPath, content.toUpperCase());
const result = yield* fs.readFileString(outputPath);
// Temp directory is automatically removed when scope exits
return result;
}).pipe(Effect.scoped);
Layer Provision
Node.js
import { FileSystem } from 'effect';
import { NodeFileSystem } from '@effect/platform-node';
import { Effect } from 'effect';
const program = Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
return yield* fs.readFileString('data.txt');
});
// Provide Node.js implementation
const runnable = program.pipe(Effect.provide(NodeFileSystem.layer));
Effect.runPromise(runnable);
Bun
import { FileSystem } from 'effect';
import { BunFileSystem } from '@effect/platform-bun';
import { Effect } from 'effect';
declare const program: Effect.Effect<string, never, FileSystem.FileSystem>;
const runnable = program.pipe(Effect.provide(BunFileSystem.layer));
Effect.runPromise(runnable);
DO
- Import from
effect - Use
yield* FileSystem.FileSystemfor service injection - Provide platform layer at entry point only
- Use scoped temp directories with
makeTempDirectoryScoped - Handle
PlatformErrorwithcatchTag("PlatformError", ...) - Use size helpers:
Size(),KiB(),MiB(),GiB(),TiB(),PiB() - Stream large files with
stream()andsink()
DON'T
- Import
node:fs,fs/promises, or platform-specific modules in business logic - Use synchronous fs operations
- Forget to cleanup temp directories (use scoped version)
- Mix platform-specific code with business logic
- Use
Date.now()- useClockservice instead (see testability requirements) - Hardcode platform-specific paths - use
Pathservice for path operations