Lazy, composable effects with typed errors
A powerful effect type inspired by ZIO for building composable, type-safe programs with dependency injection
Lazy, composable effects with typed errors and dependency injection.
IO represents a lazy effect that:
R (dependencies)E (typed errors)A on successNothing runs until explicitly executed.
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
| Method | Description |
|---|---|
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.unit | Effect that succeeds with void |
IO.never | Effect that never completes |
// 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);
// 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
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();
// 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);
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
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 }));
// 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()),
);
// 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);
| Feature | IO<R,E,A> | Task |
|---|---|---|
| Typed Errors | Yes (E parameter) | No |
| Dependency Injection | Yes (R parameter) | No |
| Cancellation | Via interrupt | Built-in |
| Progress Tracking | No | Yes |
| Best For | Complex apps with DI | Simple async with cancellation |
See full API documentation at functype API docs