Effect API Journey — Cheatsheet

A single-page reference for the Effect Path API. Two columns: how we wrote it before, how we write it now.

Scope

  • All Path factory methods and the four core operations (map, via, recovery, zipWith)
  • ForPath comprehension shape and tuple-binding semantics
  • Effect Contexts: ErrorContext, ConfigContext, MutableContext
  • Service-integration patterns (@GeneratePathBridge)
  • Focus-Effect bridge basics

Pick the right Path type

The shape we haveThe Path we wantFactory
A value that may be nullMaybePath<A>Path.maybe(nullable)
A value we know is presentMaybePath<A>Path.just(value)
Explicit absenceMaybePath<A>Path.nothing()
A value or a typed errorEitherPath<E, A>Path.right(v) / Path.left(e) / Path.either(either)
A Supplier that may throwTryPath<A>Path.tryOf(() -> ...)
A side-effecting computation, deferredIOPath<A>Path.io(() -> ...)
A virtual-thread async computationVTaskPath<A>Path.vtask(() -> ...)
An existing Optional<A>OptionalPath<A>Path.optional(opt)

Core operations (work on every Path)

PatternImperative JavaHigher-Kinded-J
Transform the success valueopt.map(f) / future.thenApply(f)path.map(f)
Chain a step that itself returns a Pathopt.flatMap(f) / future.thenCompose(f)path.via(f) (flatMap is an alias)
Replace error with a valuetry { ... } catch (E e) { return default; }path.recover(err -> default)
Replace error with another pathtry { ... } catch (E e) { return retry(); }path.recoverWith(err -> alt)
Use an alternative when this failsOptional.or(() -> alt)path.orElse(() -> alt)
Translate the error typemanual mappath.mapError(e -> e2)
Combine two independent pathsCompletableFuture.allOf then unpackpathA.zipWith(pathB, combiner)

map vs via — the canonical decision

Function shapeReach forResult shape
A -> B (plain value)mapPath<B>
A -> Path<B> (wrapped value)via (or flatMap)Path<B> (flattened)
A -> Path<B> but we used map(compile error or worse)Path<Path<B>> (silently nested)

When in doubt, follow the type: if the lambda body returns a Path, the call is via.


ForPath at a glance

ForPath.from(initialPath)              // start with a Path
  .from(value -> dependentPath(value)) // bind another Path step
  .let(value -> pureFunction(value))   // bind a pure value (no new effect)
  .yield((step1, step2, step3) -> ...) // combine all bound values
  • Each .from(...) lambda after the first receives a tuple of previously bound values; reach in with t._1(), t._2(), ...
  • .yield(...) is the only place where every binding is in scope by name.
  • Short-circuits on the first failing step (Nothing / Left / Failure), exactly like a plain chain of vias.

When to reach for ForPath: three or more dependent steps, or when intermediate values need to be referenced from later steps.


Effect Contexts

ContextWrapsUse for
ErrorContext<?, E, A>EitherT (typed error + IO)Async work that may fail with a domain error
ConfigContext<?, R, A>ReaderTThreading configuration through a workflow without DI
MutableContext<?, S, A>StateTWorkflow-local state without ThreadLocal or mutable fields

All three carry the same map / via / recover surface as the Path API; the suffix differs only in how we run the workflow at the boundary:

errorCtx.runIO().unsafeRun();         // -> Either<E, A>
configCtx.runWithSync(config);        // -> A
mutableCtx.runWith(initial)           // -> StateTuple<S, A>
         .unsafeRun();

Service integration

@GeneratePathBridge
public interface UserService {
    @PathVia
    Optional<User> findById(Long id);

    @PathVia
    Either<Error, User> createUser(CreateUserRequest req);
}

The annotation processor generates UserServicePaths with:

Original returnGenerated wrapper
Optional<T>OptionalPath<T> (or MaybePath<T> with @PathVia(MAYBE))
Either<E, T>EitherPath<E, T>
T (with @PathVia(IO))IOPath<T>
CompletableFuture<T>VTaskPath<T> (or CompletableFuturePath<T>)

The wrapper is exactly the one-liner shown in Tutorial 02 Exercise 6 — generated so we never have to write it.


Focus-Effect bridge

EitherPath<Error, User>
  .focus(addressPath)        // narrow to the Address inside the User
  .focus(cityPath)            // narrow further to the city String
  .map(String::toUpperCase);  // transform the focused field

Errors propagate through focus(...) unchanged. For more complex navigation use ForPath with .focus(...) (Tutorial 14 of the Optics journey covers the full bridge).


The boundary rule

unsafeRun() (and equivalents like runWithSync) belong at the edge of the program. Build the entire workflow as a value, then run once.

// Right
ErrorContext<?, E, A> workflow =
    step1().via(this::step2).via(this::step3).map(this::finalize);
return workflow.runIO().unsafeRun();

// Wrong
A intermediate = step1().runIO().unsafeRun();   // executes too early
return step2(intermediate).runIO().unsafeRun(); // runs the IO twice

Where this lands in One Line, Six Layers

The Effect API is the layer that lets us write the One Line, Six Layers expression at all:

   repo.find(id)              .toEitherPath()      .focus().attributes().at(key)
   └── Effect Path ───────────┤                    └── Optic ─ via Focus DSL ───┐
       MaybePath, EitherPath, │                                                 │
       TryPath, IOPath, ...   │                                                 │
                              │                                                 │
                              └── Tutorial 01: every Path conversion lives in   │
                                  this one method shape                         │
                                                                                │
   .modify(spec::validateAndCoerce)             .flatMap(repo::save);           │
   └── map (under the optic) ────┐              └── via / flatMap ──────────────┘
       Tutorial 01 (map)         │                  Tutorial 01 (via)
                                │
                                └── ForPath / Contexts (Tutorial 02) flatten
                                    multi-step versions of this shape

See also: Effect Path Overview · ForPath Comprehension · Effect Contexts · One Line, Six Layers