FileSystem
The FileSystem service is Effect’s abstraction over the file system. Every
method returns an Effect that fails with a PlatformError
instead of throwing, so missing files, permission errors, and bad arguments all
flow through the typed error channel. You depend on the abstract FileSystem
interface and provide a concrete implementation — NodeFileSystem.layer on
Node.js — at the edge of your program.
import { NodeFileSystem } from "@effect/platform-node"import { Context, Effect, FileSystem, Layer, PlatformError } from "effect"
// A small service that persists JSON snapshots to a directory.class SnapshotStore extends Context.Service<SnapshotStore, { readonly save: (name: string, value: unknown) => Effect.Effect<void, PlatformError.PlatformError> readonly load: (name: string) => Effect.Effect<unknown, PlatformError.PlatformError>}>()("app/SnapshotStore") { static readonly layer = Layer.effect( SnapshotStore, Effect.gen(function*() { const fs = yield* FileSystem.FileSystem const dir = "snapshots"
// Ensure the target directory exists. `recursive` makes this a no-op // when the directory is already present, like `mkdir -p`. yield* fs.makeDirectory(dir, { recursive: true })
const save = (name: string, value: unknown) => // `writeFileString` writes (and creates) the file in one call. fs.writeFileString(`${dir}/${name}.json`, JSON.stringify(value, null, 2))
const load = (name: string) => fs.readFileString(`${dir}/${name}.json`).pipe( Effect.map((text) => JSON.parse(text) as unknown) )
return { save, load } as const }) ).pipe( // The store needs a concrete FileSystem; wire in the Node implementation. Layer.provide(NodeFileSystem.layer) )}
const program = Effect.gen(function*() { const store = yield* SnapshotStore yield* store.save("user-42", { id: 42, name: "Ada" }) const restored = yield* store.load("user-42") yield* Effect.log(restored)}).pipe(Effect.provide(SnapshotStore.layer))Reading and writing
Section titled “Reading and writing”The most common operations come in byte and string flavours. Use the String
variants when you are working with text and the raw variants when you need exact
bytes:
import { Effect, FileSystem } from "effect"
const program = Effect.gen(function*() { const fs = yield* FileSystem.FileSystem
// Text helpers return / accept `string`. yield* fs.writeFileString("notes.txt", "first line\n") const text = yield* fs.readFileString("notes.txt")
// Byte helpers return / accept `Uint8Array`. const bytes: Uint8Array = yield* fs.readFile("notes.txt")
// `flag: "a"` appends instead of truncating. yield* fs.writeFileString("notes.txt", "second line\n", { flag: "a" })
yield* Effect.log(`${text.length} chars, ${bytes.length} bytes`)})Inspecting the file system
Section titled “Inspecting the file system”exists, stat, and readDirectory let you query the file system before
acting on it. Note that timestamps on stat are wrapped in Option because not
every platform reports them:
import { Effect, FileSystem, Option } from "effect"
const describe = Effect.fn("describe")(function*(path: string) { const fs = yield* FileSystem.FileSystem
if (!(yield* fs.exists(path))) { return `${path} does not exist` }
// `stat` returns a `File.Info`: type, size (a branded bigint), mode, and // optional timestamps. const info = yield* fs.stat(path)
const modified = Option.match(info.mtime, { onNone: () => "unknown", onSome: (date) => date.toISOString() })
return `${info.type} — ${info.size} bytes — modified ${modified}`})To enumerate a directory, readDirectory returns the entry names; pass
recursive: true to walk nested directories:
import { Effect, FileSystem } from "effect"
const listTree = Effect.gen(function*() { const fs = yield* FileSystem.FileSystem return yield* fs.readDirectory("src", { recursive: true })})Streaming large files
Section titled “Streaming large files”Reading a multi-gigabyte file into memory with readFile is wasteful. The
stream method returns a Stream of byte chunks so you can
process a file incrementally, and sink gives you a writable counterpart. Here
we count the lines of a file without ever holding it all in memory:
import { Effect, FileSystem, Stream } from "effect"
const countLines = Effect.fn("countLines")(function*(path: string) { const fs = yield* FileSystem.FileSystem
return yield* fs.stream(path).pipe( Stream.decodeText(), // Uint8Array chunks -> string chunks Stream.splitLines, // re-chunk on line boundaries Stream.runFold(() => 0, (count) => count + 1) )})You can tune chunkSize, offset, and bytesToRead on stream when you need
to read a specific window of a file.
Temporary files and scopes
Section titled “Temporary files and scopes”Temporary files and directories are tied to a Scope
through the *Scoped variants, so they are deleted automatically when the scope
closes — even if the effect fails:
import { Effect, FileSystem } from "effect"
const withScratch = Effect.gen(function*() { const fs = yield* FileSystem.FileSystem
// Created inside the surrounding scope; removed when the scope closes. const dir = yield* fs.makeTempDirectoryScoped({ prefix: "build-" })
yield* fs.writeFileString(`${dir}/manifest.json`, "{}") yield* Effect.log(`working in ${dir}`)}).pipe( // `Effect.scoped` provides and closes the scope, triggering cleanup. Effect.scoped)Opening a file with fs.open is similarly scoped: the returned File handle is
closed when the scope ends, so you never leak file descriptors.
Watching for changes
Section titled “Watching for changes”watch returns a Stream of WatchEvent values (Create, Update,
Remove) so you can react to file system activity — handy for dev tooling and
hot reload:
import { Effect, FileSystem, Stream } from "effect"
const watchSrc = Effect.gen(function*() { const fs = yield* FileSystem.FileSystem
yield* fs.watch("src").pipe( Stream.runForEach((event) => Effect.log(`${event._tag}: ${event.path}`) ) )})Handling errors
Section titled “Handling errors”Because failures are values, narrow them with Effect.catchTag. A
PlatformError exposes a human-readable message and a structured reason
(either a SystemError or a BadArgument):
import { Effect, FileSystem } from "effect"
const readOrDefault = Effect.fn("readOrDefault")(function*(path: string) { const fs = yield* FileSystem.FileSystem return yield* fs.readFileString(path)}).pipe( // Fall back to an empty document when the read fails for any reason. Effect.catchTag("PlatformError", (error) => Effect.as(Effect.logWarning(`read failed: ${error.message}`), "") ))See Manipulate paths with Path for building the paths you pass to these methods in a cross-platform way.