Complete reference for Roux's Effect system.
- Effect Constructors
- Transformation Operators
- Error Handling
- Concurrency
- Scheduling
- Capabilities
- Runtime Execution
- Helper Types
Create an effect that succeeds with a value.
Effect<Throwable, String> effect = Effect.succeed("hello");
// Result: "hello"Type: <E extends Throwable, A> Effect<E, A>
Create an effect that fails with an error.
Effect<IOException, String> effect = Effect.fail(new IOException("error"));
// Throws: IOException("error")Type: <E extends Throwable, A> Effect<E, A>
Create an effect from a lazy computation. The computation is executed when the effect runs.
Effect<Throwable, String> effect = Effect.suspend(() -> {
System.out.println("Computing...");
return "result";
});
// Prints "Computing..." only when effect runsType: <E extends Throwable, A> Effect<E, A>
Use Cases:
- Lazy evaluation
- Wrapping side effects
- Deferring expensive computations
Create an effect using generator-style imperative code with capabilities.
Effect<Throwable, String> effect = Effect.generate(ctx -> {
String data = ctx.perform(new FetchData("url"));
ctx.perform(new LogInfo("Got: " + data));
return data.toUpperCase();
}, handler);Type: <E extends Throwable, A> Effect<E, A>
Generator Context Methods:
ctx.perform(capability)- Execute a capabilityctx.lift(capability)- Convert capability to effect without executingctx.call(operation)- Execute a direct operationctx.yield(effect)- Execute another effect
Build an effect using generator-style imperative code when no capability handler is needed — only ctx.yield() and ctx.call() are used. Removes the dead CapabilityHandler.builder().build() argument required by Effect.generate().
Effect<IOException, String> pipeline = Effect.effect(ctx -> {
String a = ctx.yield(fetchA()); // sequence effects, propagate typed errors
String b = ctx.yield(fetchB());
return a + b;
});Type: <E extends Throwable, A> Effect<E, A>
Use
Effect.generate(generator, handler)when capabilities (ctx.perform()) are also needed.
Lift a capability into an effect. Handler is provided at runtime.
Effect<Throwable, User> effect = Effect.from(new GetUser("123"));
// Handler provided via runtime.unsafeRunWithHandler(effect, handler)Type: <E extends Throwable, R> Effect<E, R>
Create a scoped effect for structured concurrency. All forked effects within the scope are automatically managed and cancelled when the scope exits.
Tip — reach for simpler tools first. For the common case of running a fixed number of tasks in parallel and combining the results,
Effects.paris shorter and requires no nesting. UseEffect.scopedwhen you need the scope handle itself (e.g., to callscope.cancelAll()conditionally mid-flight).// Common case — just use Effects.par Effect<Throwable, Summary> summary = Effects.par( fetchUser(id), fetchOrders(id), fetchPrefs(id), Summary::new );
When you do need a scope (conditional cancellation, hierarchical control):
Effect<Throwable, String> effect = Effect.scoped(scope -> {
var fiber1 = task1.forkIn(scope);
var fiber2 = task2.forkIn(scope);
return fiber1.flatMap(f1 -> f1.join().flatMap(r1 -> {
if (shouldStop(r1)) {
// cancel remaining tasks — this is why we have the scope
return scope.cancelAll().flatMap(__ -> Effect.succeed(r1));
}
return fiber2.flatMap(f2 -> f2.join().map(r2 -> combine(r1, r2)));
}));
// Both tasks automatically cancelled if scope exits with an error
});Type: <E extends Throwable, A> Effect<E, A>
Scope Methods:
scope.fork(effect)- Fork an effect within the scopescope.cancelAll()- Manually cancel all forked effectsscope.isCancelled()- Check if scope is cancelled
Guarantees:
- ✅ All forked effects are tracked
- ✅ Automatic cancellation on scope exit (success, error, or early return)
- ✅ No leaked threads or resources
- ✅ Built on Java's
StructuredTaskScope(JEP 453)
See: Structured Concurrency Guide for the full decision guide and patterns.
Transform the success value of an effect.
Effect<Throwable, String> effect = Effect.succeed(42)
.map(n -> "Number: " + n);
// Result: "Number: 42"Type: <B> Effect<E, B>
Example - Multiple transformations:
Effect<Throwable, Integer> result = Effect.succeed("123")
.map(Integer::parseInt)
.map(n -> n * 2)
.map(n -> n + 10);
// Result: 256Chain effects sequentially. The function receives the success value and returns a new effect.
Effect<Throwable, User> effect = Effect.succeed("123")
.flatMap(id -> fetchUser(id))
.flatMap(user -> enrichUser(user));Type: <B> Effect<E, B>
Example - Sequential workflow:
Effect<Throwable, Order> placeOrder = Effect.succeed(userId)
.flatMap(id -> getUser(id))
.flatMap(user -> validateUser(user))
.flatMap(user -> createOrder(user))
.flatMap(order -> chargePayment(order))
.flatMap(order -> sendConfirmation(order));Recover from all errors by providing a fallback effect.
Effect<Throwable, String> effect = fetchData()
.catchAll(error -> {
log.error("Failed: " + error);
return Effect.succeed("default");
});Type: Effect<E, A>
Example - Retry with fallback:
Effect<Throwable, String> resilient = fetchFromPrimary()
.catchAll(e1 -> fetchFromSecondary())
.catchAll(e2 -> Effect.succeed("cached-value"));Provide a fallback effect if this effect fails.
Effect<Throwable, String> effect = fetchFromCache()
.orElse(fetchFromDatabase())
.orElse(Effect.succeed("default"));Type: Effect<E, A>
Transform the error type of an effect.
Effect<AppError, String> effect = Effect.<IOException, String>fail(new IOException("IO error"))
.mapError(ioError -> new AppError("Network failed", ioError));Type: <E2 extends Throwable> Effect<E2, A>
Example - Error normalization:
Effect<DomainError, User> normalized = fetchUser(id)
.mapError(e -> switch(e) {
case IOException io -> new DomainError.NetworkError(io);
case SQLException sql -> new DomainError.DatabaseError(sql);
default -> new DomainError.UnknownError(e);
});Widen the error type to Throwable. This is a safe operation useful for composing effects with different error types.
Effect<IOException, String> io = fetchFile();
Effect<SQLException, User> sql = queryUser();
// Widen both to Throwable for composition
Effect<Throwable, String> combined = io.widen()
.flatMap(data -> sql.widen().map(user -> process(data, user)));Type: Effect<Throwable, A>
Use case: Composing effects with different specific error types.
Narrow the error type to a more specific exception type.
ClassCastException.
// Library returns generic Throwable
Effect<Throwable, Config> generic = loadFromLibrary();
// You know it only throws ConfigException
Effect<ConfigException, Config> specific = generic.narrow();
// Now you can handle ConfigException specifically
Effect<ConfigException, Config> handled = specific.catchAll(e ->
Effect.succeed(defaultConfig())
);Type: <E2 extends E> Effect<E2, A>
Use case: Type refinement when you know the actual error type is more specific.
Convert an effect into one that cannot fail, wrapping the result in Either<E, A>.
Effect<Throwable, Either<IOException, String>> safe = fetchData().attempt();
Either<IOException, String> result = runtime.unsafeRun(safe);
switch (result) {
case Either.Left<IOException, String> left ->
System.err.println("Error: " + left.value());
case Either.Right<IOException, String> right ->
System.out.println("Success: " + right.value());
}Type: Effect<Throwable, Either<E, A>>
Run an effect on a separate virtual thread, returning a Fiber handle.
Effect<Throwable, Fiber<Throwable, String>> fiberEffect = longRunningTask.fork();
Fiber<Throwable, String> fiber = runtime.unsafeRun(fiberEffect);
String result = runtime.unsafeRun(fiber.join());Type: Effect<Throwable, Fiber<E, A>>
Fiber Methods:
join()- Wait for fiber to complete and get resultinterrupt()- Cancel the fiberid()- Get unique fiber ID
Fork an effect within a specific scope for structured concurrency.
Effect<Throwable, String> effect = Effect.scoped(scope -> {
Fiber<Throwable, String> fiber1 = scope.fork(task1);
Fiber<Throwable, String> fiber2 = scope.fork(task2);
String r1 = fiber1.join();
String r2 = fiber2.join();
return r1 + r2;
// Both fibers auto-cancelled if scope exits early
});Type: Effect<Throwable, Fiber<E, A>>
Run two effects in parallel and combine their results.
Effect<Throwable, Dashboard> dashboard = fetchUser("123")
.zipPar(fetchOrders("123"), (user, orders) ->
new Dashboard(user, orders)
);Type: <B, C> Effect<Throwable, C>
Example - Parallel data fetching:
record Summary(User user, List<Order> orders, Preferences prefs) {}
Effect<Throwable, Summary> summary = fetchUser(id)
.zipPar(fetchOrders(id), Tuple2::new)
.zipPar(fetchPreferences(id), (userOrders, prefs) ->
new Summary(userOrders.first(), userOrders.second(), prefs)
);Run 2, 3, or 4 effects in parallel with a combiner function.
import static com.cajunsystems.roux.Effects.*;
// 2 effects
Effect<Throwable, Result> result = par(
fetchUser(id),
fetchOrders(id),
Result::new
);
// 3 effects
Effect<Throwable, Dashboard> dashboard = par(
fetchUser(id),
fetchOrders(id),
fetchPreferences(id),
Dashboard::new
);
// 4 effects
Effect<Throwable, Summary> summary = par(
fetchUser(id),
fetchOrders(id),
fetchPreferences(id),
fetchNotifications(id),
Summary::new
);Types:
par(ea, eb, f)- 2 effectspar(ea, eb, ec, f)- 3 effectspar(ea, eb, ec, ed, f)- 4 effects
Apply a function to every element of a list to produce effects, run them all in parallel, and collect results in input order. Fails fast on the first error.
import static com.cajunsystems.roux.Effects.*;
// Fetch all users in parallel — fails fast if any fetch fails
Effect<Throwable, List<User>> users =
parTraverse(userIds, id -> fetchUser(id).<Throwable>toEffect());Type: <E extends Throwable, A, B> Effect<Throwable, List<B>>
Like parTraverse but wraps each result in Either, so failures are collected alongside successes rather than short-circuiting the whole computation.
// Process all items — collect both successes and failures
Effect<Throwable, List<Either<Throwable, Result>>> outcomes =
parTraverseEither(items, item -> process(item).<Throwable>toEffect());
List<Either<Throwable, Result>> results = runtime.unsafeRun(outcomes);
long successCount = results.stream().filter(Either::isRight).count();
long failureCount = results.stream().filter(Either::isLeft).count();Type: <E extends Throwable, A, B> Effect<Throwable, List<Either<E, B>>>
RetryPolicy handles the failure path (retry on error). Schedule handles the success path — repeat an effect on a cadence, while a predicate holds, or for a fixed number of iterations, and optionally accumulate the outputs.
Schedule.<Status>fixed(Duration.ofSeconds(2)) // fixed delay between runs
Schedule.<Integer>exponential(Duration.ofMillis(100)) // doubling delay: 100ms, 200ms, 400ms...
Schedule.<String>immediate() // no delayschedule.recurs(10) // stop after 10 repetitions (initial run + 10)
schedule.whileOutput(s -> !s.done()) // stop when predicate returns false
schedule.untilOutput(s -> s.done()) // stop when predicate returns true
schedule.maxDelay(Duration.ofSeconds(30)) // cap computed delay
schedule.jittered(0.2) // ±20% random jitter on delay// Collect all outputs into a List<A>
Schedule<Status, List<Status>> collecting = schedule.collect();Effect<Throwable, B> result = schedule.repeat(effect);The repeat loop is stack-safe — it builds a flatMap chain executed by the trampolined runtime, so large recurs counts don't overflow the stack.
Polling loop:
Schedule<Status, List<Status>> poll = Schedule
.<Status>fixed(Duration.ofSeconds(2))
.recurs(30)
.whileOutput(s -> !s.isDone())
.collect();
Effect<Throwable, List<Status>> history = poll.repeat(checkJobStatus);Composing with RetryPolicy (retry failures, repeat on success):
Schedule<Integer, Integer> schedule = Schedule
.<Integer>exponential(Duration.ofMillis(50))
.maxDelay(Duration.ofSeconds(5))
.recurs(10);
Effect<Throwable, Integer> resilient = schedule.repeat(
unstableEffect.retry(RetryPolicy.immediate().maxAttempts(3))
);Convert a capability to an effect. Handler is implicit from execution context.
sealed interface MyCapability<R> extends Capability<R> {
record GetUser(String id) implements MyCapability<User> {}
record GetOrders(String userId) implements MyCapability<List<Order>> {}
}
// Convert to effect
Effect<Throwable, User> userEffect = new MyCapability.GetUser("123")
.toEffect()
.map(user -> enrichUser(user))
.catchAll(e -> Effect.succeed(User.GUEST));
// Run with handler
User user = runtime.unsafeRunWithHandler(userEffect, handler);Type: <E extends Throwable> Effect<E, R>
Benefits:
- All Effect operators work (map, flatMap, retry, timeout, zipPar, etc.)
- Handler provided at runtime
- Clean, composable API
Implement capability interpreters.
class ProductionHandler implements CapabilityHandler<MyCapability<?>> {
@Override
@SuppressWarnings("unchecked")
public <R> R handle(MyCapability<?> capability) throws Exception {
return switch (capability) {
case MyCapability.GetUser get ->
(R) httpClient.get("/users/" + get.id());
case MyCapability.GetOrders getOrders ->
(R) database.query("SELECT * FROM orders WHERE user_id = ?",
getOrders.userId());
};
}
}Handler Composition:
// Combine multiple handlers
CapabilityHandler<Capability<?>> combined = CapabilityHandler.compose(
httpHandler,
dbHandler,
logHandler
);
// Fallback chain
CapabilityHandler<Capability<?>> withFallback = primaryHandler
.orElse(secondaryHandler)
.orElse(defaultHandler);Use capabilities in generator-style code.
Effect<Throwable, Result> workflow = Effect.generate(ctx -> {
// Perform capability directly
User user = ctx.perform(new GetUser("123"));
// Lift capability to effect for composition
Effect<Throwable, List<Order>> ordersEffect = ctx.lift(new GetOrders(user.id()))
.map(orders -> orders.stream()
.filter(Order::isActive)
.toList());
// Yield to execute the effect
List<Order> orders = ctx.yield(ordersEffect);
// Call direct operations
String formatted = ctx.call(() -> formatResult(user, orders));
return new Result(user, orders, formatted);
}, handler);Context Methods:
perform(capability)- Execute capability immediatelylift(capability)- Convert to effect without executingcall(operation)- Execute direct operationyield(effect)- Execute another effecthandler()- Get current handler
Execute an effect synchronously, throwing errors.
EffectRuntime runtime = DefaultEffectRuntime.create();
String result = runtime.unsafeRun(fetchData());
// Blocks until complete, throws on errorType: <E extends Throwable, A> A throws E
Execute an effect with a capability handler.
EffectRuntime runtime = DefaultEffectRuntime.create();
CapabilityHandler<Capability<?>> handler = new ProductionHandler();
Effect<Throwable, User> effect = new GetUser("123")
.toEffect()
.map(user -> enrichUser(user));
User user = runtime.unsafeRunWithHandler(effect, handler);Type: <E extends Throwable, A> A throws E
Execute an effect asynchronously with callbacks.
CancellationHandle handle = runtime.runAsync(
longRunningTask,
result -> System.out.println("Success: " + result),
error -> System.err.println("Error: " + error)
);
// Cancel if needed
handle.cancel();
// Wait for completion
handle.await();Type: <E extends Throwable, A> CancellationHandle
CancellationHandle Methods:
cancel()- Cancel the running effectisCancelled()- Check if cancelledawait()- Block until completeawait(timeout)- Block with timeout
Represents a value that can be either Left (error) or Right (success).
Effect<Throwable, Either<IOException, String>> safe = fetchData().attempt();
Either<IOException, String> result = runtime.unsafeRun(safe);
// Pattern matching
String value = switch (result) {
case Either.Left<IOException, String> left -> "Error: " + left.value();
case Either.Right<IOException, String> right -> right.value();
};Simple tuple types for combining values.
import com.cajunsystems.roux.Effects.Tuple2;
import com.cajunsystems.roux.Effects.Tuple3;
Tuple2<String, Integer> pair = new Tuple2<>("hello", 42);
String first = pair.first();
Integer second = pair.second();
Tuple3<String, Integer, Boolean> triple = new Tuple3<>("hello", 42, true);Function types for 3 and 4 arguments.
import com.cajunsystems.roux.Effects.Function3;
import com.cajunsystems.roux.Effects.Function4;
Function3<String, Integer, Boolean, Result> f3 = (a, b, c) ->
new Result(a, b, c);
Function4<String, Integer, Boolean, Double, Result> f4 = (a, b, c, d) ->
new Result(a, b, c, d);Effect<Throwable, Order> placeOrder = Effect.succeed(userId)
.flatMap(id -> validateUser(id))
.flatMap(user -> createOrder(user))
.flatMap(order -> processPayment(order))
.flatMap(order -> sendConfirmation(order))
.catchAll(error -> {
logError(error);
return Effect.fail(new OrderError(error));
});Effects.par is the idiomatic way to run a fixed number of independent effects in parallel and combine the results. No nesting, no fiber management:
import static com.cajunsystems.roux.Effects.*;
// 3 tasks in parallel — flat, readable
Effect<Throwable, Dashboard> dashboard = par(
fetchUser(userId),
fetchOrders(userId),
fetchAnalytics(userId),
Dashboard::new
);For parallel work over a variable-length collection, use parTraverse:
// Fetch all users in parallel
Effect<Throwable, List<User>> users = parTraverse(userIds, id -> fetchUser(id));For parallel work with imperative logic between fork and join, use Effect.effect:
Effect<Throwable, Dashboard> dashboard = Effect.effect(ctx -> {
var userF = ctx.yield(fetchUser(userId).fork());
var ordersF = ctx.yield(fetchOrders(userId).fork());
var analyticsF = ctx.yield(fetchAnalytics(userId).fork());
// Log something between fork and join
logger.info("Waiting for dashboard data for {}", userId);
return new Dashboard(
ctx.yield(userF.join()),
ctx.yield(ordersF.join()),
ctx.yield(analyticsF.join())
);
});// Simple retry — 3 extra attempts, no delay
Effect<Throwable, String> withRetry = fetchData()
.retry(3)
.catchAll(e -> Effect.succeed("default"));
// Retry with exponential-backoff delay
Effect<Throwable, String> withBackoff = fetchData()
.retryWithDelay(3, Duration.ofMillis(500))
.catchAll(e -> Effect.succeed("default"));For a single resource, use Resource.make + .use():
Resource<Connection> connResource = Resource.make(
Effect.suspend(() -> pool.acquire()),
conn -> Effect.runnable(conn::close)
);
Effect<Throwable, Result> query = connResource.use(conn ->
Effect.blocking(() -> conn.execute("SELECT ..."))
);
// Connection always closed — success, failure, or cancellationFor multiple resources, use the Resources fluent builder to avoid nesting:
// ✅ Flat multi-resource acquisition
Effect<Throwable, Result> program = Resources
.with(connectionResource)
.and(statementResource)
.use((conn, stmt) -> Effect.blocking(() -> stmt.executeQuery()));For raw I/O with a single resource, a try-finally inside blocking is also fine:
Effect<Throwable, String> readFile = Effect.blocking(() -> {
FileHandle file = openFile("data.txt");
try {
return readContent(file);
} finally {
file.close(); // guaranteed regardless of success or failure
}
});// Define capabilities
sealed interface AppCapability<R> extends Capability<R> {
record FetchUser(String id) implements AppCapability<User> {}
record SaveOrder(Order order) implements AppCapability<Void> {}
record SendEmail(String to, String body) implements AppCapability<Void> {}
}
// Business logic (pure, testable)
Effect<Throwable, Result> workflow = new AppCapability.FetchUser("123")
.toEffect()
.flatMap(user -> new AppCapability.SaveOrder(createOrder(user)).toEffect())
.flatMap(v -> new AppCapability.SendEmail(user.email(), "Order created").toEffect())
.map(v -> new Result("success"));
// Production
runtime.unsafeRunWithHandler(workflow, productionHandler);
// Testing
runtime.unsafeRunWithHandler(workflow, testHandler);Roux's Effect system is fully type-safe:
// Error type is tracked
Effect<IOException, String> io = fetchFile();
Effect<SQLException, User> sql = queryUser();
// Composition maintains type safety - use widen() for convenience
Effect<Throwable, String> combined = io
.widen() // Widen error type from IOException to Throwable
.flatMap(data -> sql.widen());
// Or use mapError for more control
Effect<Throwable, String> combined2 = io
.mapError(e -> (Throwable) e)
.flatMap(data -> sql.mapError(e -> (Throwable) e));
// Narrow error types when you know the specific type (unsafe cast)
Effect<Throwable, Config> generic = loadFromLibrary();
Effect<ConfigException, Config> specific = generic.narrow();
// Compiler catches type errors
// io.flatMap(data -> sql); // ❌ Won't compile - error types don't match- Virtual Threads: All effects run on virtual threads (Project Loom)
- Lazy Evaluation: Effects are descriptions, not executions
- Zero Overhead: Effect combinators are just data structures until executed
- Structured Concurrency: Automatic cleanup of forked effects
- Cancellation: Built-in support for interrupting running effects
-
Use
succeedandsuspendfor pure values vs side effectsEffect.succeed(42) // Pure value Effect.suspend(() -> readFile()) // Side effect
-
Prefer
flatMapfor sequential;Effects.par/parTraversefor parallela.flatMap(x -> b.map(y -> combine(x, y))) // Sequential Effects.par(a, b, (x, y) -> combine(x, y)) // Parallel (fixed arity) Effects.parTraverse(items, item -> process(item)) // Parallel (variable list)
-
Use capabilities for testability
// Instead of direct calls Effect.suspend(() -> httpClient.get(url)) // Use capabilities new HttpGet(url).toEffect()
-
Handle errors explicitly
effect .catchAll(e -> fallback) .orElse(defaultEffect)
-
Use
Resourcefor resource management;Effect.scopedonly when you need cancellation control// Single resource connResource.use(conn -> query(conn)) // Multiple resources — flat, not nested Resources.with(connResource).and(stmtResource).use((conn, stmt) -> query(conn, stmt)) // Effect.scoped only when you need scope.cancelAll() or scope.isCancelled()
- Capabilities Guide - Algebraic effects system
- Capability Recipes - Common patterns
- Custom Capabilities Example - Complete example