Railway Diagrams
The railway diagram is the signature visualization of effect-analyzer. Inspired by the railway-oriented programming pattern, it renders your Effect program as a straight-line happy path with error branches forking off at failure points.
flowchart LR A["AccountService"] -->|ok| B["AuditLog"] B -->|ok| C["accounts.getBalance"] C -->|ok| D["decision"] D -->|ok| E["accounts.debit"] E -->|ok| F["accounts.credit"] F -->|ok| G["audit.record"] G -->|ok| Done((Success)) D -->|err| DE["InsufficientFundsError"]
import { Context, Effect } from 'effect';
export class AccountNotFoundError { readonly _tag = 'AccountNotFoundError'; constructor(readonly accountId: string) {}}
export class InsufficientFundsError { readonly _tag = 'InsufficientFundsError'; constructor( readonly available: number, readonly requested: number, ) {}}
export class AccountService extends Context.Tag('AccountService')< AccountService, { readonly getBalance: ( accountId: string, ) => Effect.Effect<number, AccountNotFoundError>; readonly debit: ( accountId: string, amount: number, ) => Effect.Effect<void, AccountNotFoundError>; readonly credit: ( accountId: string, amount: number, ) => Effect.Effect<void, AccountNotFoundError>; }>() {}
export class AuditLog extends Context.Tag('AuditLog')< AuditLog, { readonly record: (message: string) => Effect.Effect<void>; }>() {}
export const transferWorkflow = ( fromAccountId: string, toAccountId: string, amount: number,) => Effect.gen(function* () { const accounts = yield* AccountService; const audit = yield* AuditLog;
const balance = yield* accounts.getBalance(fromAccountId);
if (balance < amount) { return yield* Effect.fail( new InsufficientFundsError(balance, amount), ); }
yield* accounts.debit(fromAccountId, amount); yield* accounts.credit(toAccountId, amount); yield* audit.record( `Transferred ${amount} from ${fromAccountId} to ${toAccountId}`, );
return { fromAccountId, toAccountId, amount, }; });The Railway Pattern
Section titled “The Railway Pattern”In a railway diagram, the main track represents the success path - the sequence of operations that execute when everything goes right. At each point where a failure can occur, an error branch splits off to show what happens on failure.
This makes it immediately clear:
- What the program does when it succeeds
- Where failures can happen
- What error types are produced at each point
Generating a Railway Diagram
Section titled “Generating a Railway Diagram”npx effect-analyze ./src/transfer.tsLinear programs pick railway in auto mode. --format mermaid-railway forces it. The default also writes transfer.effect-analysis.md with the same diagram.
Steps are labelled with the callee (accounts.getBalance), not a yield binding. Error nodes use the TaggedError _tag when the class has one (Data.TaggedError("NOT_FOUND") renders as NOT_FOUND).
Error Taps
Section titled “Error Taps”tapError, tapErrorTag, tapErrorCause, tapDefect and onError run a callback on the error rail and let the failure continue. The diagram draws each one as a dotted branch off the step it guards, labelled with what the callback calls:
yield* bank.charge.pipe(Effect.tapError(() => store.release(id)))flowchart LR A["Store"] -->|ok| B["Bank"] B -->|ok| C["bank.charge"] C -->|ok| Done((Success)) C -->|err| CE["Declined"] CE -.->|tapError| CT0["store.release"]
When the step has no typed error to draw, the branch starts from the step itself.
You get the same branch from the data-first form (Effect.tapError(bank.charge, f)) and from a callback passed by reference (Effect.tapError(store.release)).
When Auto Mode Selects Railway
Section titled “When Auto Mode Selects Railway”Auto mode picks the railway diagram as the baseline when your program has:
- Low cyclomatic complexity - few branching points
- Linear structure - mostly sequential
yield*steps in a generator - No parallel or race patterns - those push auto mode toward the concurrency view
Programs with Effect.gen and a series of yields are the ideal fit for railway diagrams.
Direction
Section titled “Direction”Railway diagrams default to left-to-right (LR) flow, which reads naturally as a timeline. Override this with the --direction flag:
npx effect-analyze ./src/transfer.ts --format mermaid-railway --direction TB| Direction | Best For |
|---|---|
LR |
Default - reads like a timeline |
TB |
Tall, narrow programs |
RL |
Right-to-left reading order |
BT |
Bottom-up flow |
Programmatic Usage
Section titled “Programmatic Usage”Generate railway diagrams through the library API:
import { analyze } from "effect-analyzer/analysis"import { renderRailwayMermaid } from "effect-analyzer/diagram"import { Effect } from "effect"
const ir = await Effect.runPromise(analyze("./src/transfer.ts").single)const diagram = renderRailwayMermaid(ir, { direction: "LR" })
console.log(diagram)Style Guide Mode
Section titled “Style Guide Mode”Enable --style-guide for cleaner diagrams that apply readability heuristics - collapsing trivial nodes, shortening labels, and reducing visual noise:
npx effect-analyze ./src/transfer.ts --format mermaid-railway --style-guideWhen to Use Other Diagrams
Section titled “When to Use Other Diagrams”Railway diagrams work best for linear, sequential programs. If your program has:
- Heavy branching - use
mermaid-decisionsor the standardmermaidflowchart - Parallel operations - use
mermaid-concurrency - Complex error handling - use Error Flows
- Many services - use Service Maps