Skip to content

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))

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`)
})

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 })
})

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 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.

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}`)
)
)
})

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.