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.
Effect<Throwable, String> effect = Effect.scoped(scope -> {
// Fork tasks within the scope
Effect<Throwable, Fiber<Throwable, String>> fiber1 = task1.forkIn(scope);
Effect<Throwable, Fiber<Throwable, String>> fiber2 = task2.forkIn(scope);
// Wait for results
return fiber1.flatMap(f1 ->
fiber2.flatMap(f2 ->
f1.join().flatMap(r1 ->
f2.join().map(r2 -> r1 + r2)
)
)
);
// Both tasks automatically cancelled if scope exits early
});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 comprehensive documentation 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));
});import static com.cajunsystems.roux.Effects.*;
Effect<Throwable, Dashboard> dashboard = par(
fetchUser(userId),
fetchOrders(userId),
fetchAnalytics(userId),
Dashboard::new
);// 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"));Use Effect.scoped to ensure cleanup runs when the scope exits. Fork the cleanup
as the last thing in the scope body so it executes after the main work is done,
or use a try-finally pattern inside a suspend:
Effect<Throwable, String> readFile = Effect.suspend(() -> {
FileHandle file = openFile("data.txt");
try {
return readContent(file);
} finally {
file.close(); // guaranteed regardless of success or failure
}
});For more complex resource lifetime management with concurrent fibers, use
Effect.scoped to ensure all forked effects are cancelled when the scope exits:
Effect<Throwable, String> program = Effect.scoped(scope -> {
return task1.forkIn(scope).flatMap(fiber1 ->
task2.forkIn(scope).flatMap(fiber2 ->
fiber1.join().flatMap(r1 ->
fiber2.join().map(r2 -> r1 + r2)
)
)
);
// Both fibers auto-cancelled if scope exits with an error
});// 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,zipParfor parallela.flatMap(x -> b.map(y -> combine(x, y))) // Sequential a.zipPar(b, (x, y) -> combine(x, y)) // Parallel
-
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 scoped for resource management
Effect.scoped(scope -> { // Resources auto-cleaned when scope exits })
- Capabilities Guide - Algebraic effects system
- Capability Recipes - Common patterns
- Custom Capabilities Example - Complete example