IO<R,E,A>

Lazy, composable effects with typed errors

A powerful effect type inspired by ZIO for building composable, type-safe programs with dependency injection

IO<R,E,A>

Lazy, composable effects with typed errors and dependency injection.

Overview

IO represents a lazy effect that:

  • Requires environment R (dependencies)
  • May fail with error E (typed errors)
  • Produces value A on success

Nothing runs until explicitly executed.

Basic Usage

import { IO } from "functype/io"

// Synchronous effect
const sync = IO.sync(() => 42)

// Async effect
const async = IO.async(async () => fetchData())

// Effect that may fail
const safe = IO.tryCatch(
  () => JSON.parse(input),
  (e) => new ParseError(e),
)

// Running effects - safe by default
const either = await sync.run() // Either<E, A> - never throws
const value = await sync.runOrThrow() // A - throws on error

// Synchronous execution
const syncEither = sync.runSync() // Either<E, A> - never throws
const syncValue = sync.runSyncOrThrow() // A - throws on error

Constructors

MethodDescription
IO.succeed(value)Effect that succeeds with value
IO.fail(error)Effect that fails with error
IO.sync(() => A)Wrap synchronous computation
IO.async(async () => A)Wrap async computation
IO.tryCatch(fn, onError)Catch exceptions as typed errors
IO.fromPromise(promise, onError)Convert Promise to IO
IO.unitEffect that succeeds with void
IO.neverEffect that never completes

Transformations

// Map over success value
io.map((x) => x * 2)

// Chain effects
io.flatMap((x) => IO.succeed(x + 1))

// Handle errors
io.mapError((e) => new WrappedError(e))
io.catchAll((e) => IO.succeed(fallback))
io.recover(defaultValue)

// Provide fallback
io.orElse(fallbackIO)

Combining Effects

// Run in parallel
IO.all([io1, io2, io3]) // All must succeed
IO.race([io1, io2]) // First to complete wins
IO.any([io1, io2, io3]) // First success wins

// Zip effects
io1.zip(io2) // [A, B]
io1.zipWith(io2, (a, b) => c) // C

// Sequential
io1.andThen(io2) // Run io2 after io1

Dependency Injection

IO has built-in dependency injection using Tags, Contexts, and Layers.

import { IO, Tag, Context } from "functype/io"

// Define service interface
interface Logger {
  log(message: string): void
}

// Create a Tag for the service
const Logger = Tag<Logger>("Logger")

// Use the service
const program = IO.service(Logger).flatMap((logger) => IO.sync(() => logger.log("Hello!")))

// Provide implementation
const result = await program.provideService(Logger, { log: console.log }).runOrThrow()

Context and Layer

// Build context with multiple services
const context = Context.empty().add(Logger, consoleLogger).add(Config, appConfig)

// Provide full context
program.provideContext(context)

// Use Layer for complex dependency graphs
const AppLayer = Layer.succeed(Logger, consoleLogger).merge(Layer.succeed(Config, appConfig))

program.provideLayer(AppLayer)

Do-Notation

Generator Syntax (IO.gen)

const program = IO.gen(function* () {
  const a = yield* IO.succeed(1)
  const b = yield* IO.succeed(2)
  const c = yield* IO.succeed(3)
  return a + b + c
})

await program.runOrThrow() // 6

Builder Syntax (IO.Do)

const program = IO.Do.bind("user", () => getUser("123"))
  .bind("posts", ({ user }) => getPosts(user.id))
  .let("count", ({ posts }) => posts.length)
  .map(({ user, posts, count }) => ({ user, posts, count }))

Resource Management

// Bracket pattern
IO.bracket(
  IO.sync(() => openFile(path)), // acquire
  (file) => IO.sync(() => file.close()), // release
  (file) => IO.sync(() => file.read()), // use
)

// Acquire/Release
IO.acquireRelease(
  IO.sync(() => openConnection()),
  (conn) => IO.sync(() => conn.close()),
)

Error Handling Patterns

// Catch specific errors
io.catch("NotFound", () => IO.succeed(null))

// Fold over success/failure
io.fold(
  (error) => `Failed: ${error}`,
  (value) => `Success: ${value}`,
)

// Ensure cleanup
io.ensuring(IO.sync(() => cleanup()))

// Retry on failure
io.retry(3)
io.retryWithDelay(3, 1000)

IO vs Task

FeatureIO<R,E,A>Task
Typed ErrorsYes (E parameter)No
Dependency InjectionYes (R parameter)No
CancellationVia interruptBuilt-in
Progress TrackingNoYes
Best ForComplex apps with DISimple async with cancellation

When to Use IO

  • Applications with complex dependencies (web servers, CLI tools)
  • When you want errors tracked in the type system
  • Resource management with guaranteed cleanup
  • Building composable, testable programs
  • When you need dependency injection without mocking frameworks

API Reference

See full API documentation at functype API docs