Skip to content

Platform

Most real programs eventually need to touch the world outside of pure computation: read a file, resolve a path, prompt the user, or shell out to another command. Effect models each of these capabilities as a service with a typed error channel, so platform access stays referentially transparent, testable, and decoupled from any particular runtime.

The core services live in the main effect package as abstract interfaces:

  • FileSystem — read and write files and directories, stream large files, and watch for changes.
  • Path — join, resolve, normalize, and parse file paths.
  • Terminal — read user input and display output on the command line.
  • ChildProcessSpawner — spawn and stream child processes (under effect/unstable/process).

Because these are abstract services, your code never depends on Node, Bun, or the browser directly — it depends only on the interface. You choose a concrete implementation at the edge of your application by providing a Layer.

On Node.js, the @effect/platform-node package supplies every implementation. Rather than wiring each service individually, NodeServices.layer bundles the file system, path, terminal, and child-process spawner into one layer:

import { NodeRuntime, NodeServices } from "@effect/platform-node"
import { Effect, FileSystem, Path } from "effect"
const program = Effect.gen(function*() {
// Pull the abstract services out of context — no Node import in sight.
const fs = yield* FileSystem.FileSystem
const path = yield* Path.Path
const target = path.join("/tmp", "effect-platform.txt")
yield* fs.writeFileString(target, "hello from Effect\n")
const contents = yield* fs.readFileString(target)
yield* Effect.log(contents.trim())
}).pipe(
// Provide all Node platform services in one shot.
Effect.provide(NodeServices.layer)
)
// `runMain` runs the program and reports failures with a clean exit code.
NodeRuntime.runMain(program)

The program above mentions Node in exactly one place — the call to NodeServices.layer. Everything else is written against the abstract services, so the same logic runs unchanged on a different platform layer or against an in-memory test implementation.

Platform operations fail in the typed error channel rather than throwing. Every file system operation fails with a PlatformError, which wraps a reason that is either a SystemError (an OS-level failure such as a missing file or permission denied) or a BadArgument (an invalid input). The Path service is simpler: its only fallible methods, fromFileUrl and toFileUrl, fail with a BadArgument directly. Because the failure is in the error channel, you handle it with the usual combinators like Effect.catchTag:

import { Effect, FileSystem } from "effect"
const readConfig = Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
return yield* fs.readFileString("config.json")
}).pipe(
// Missing-file failures surface as a typed PlatformError, not an exception.
Effect.catchTag("PlatformError", () => Effect.succeed("{}"))
)

NodeServices.layer is the convenient default, but each service also has its own narrow layer when you only need one capability:

  • NodeFileSystem.layer provides FileSystem.
  • NodePath.layer provides Path (with layerPosix / layerWin32 variants).
  • NodeTerminal.layer provides Terminal.
  • NodeChildProcessSpawner.layer provides ChildProcessSpawner.

Continue with the page for each capability: