11 - Option and Result
Not every value needs Effect. Option<A> represents a value that may be missing; Result<R, L> represents a synchronous success-or-failure as plain data.
Both are pure data types: no laziness, no async, no requirements. Reach for them when Effect would be heavier than the problem.
What you’ll work through
index.test.ts contains four beats. Two cover Option (constructors, operators), one covers Result, and the last shows how Option/Result yield directly inside Effect.gen — short-circuiting on None or Left without a manual conversion.
By the end you should know which of the three types to reach for: Option for “value or nothing”, Result for “sync success or failure”, Effect for anything lazy, async, or service-dependent.
Three Levels of Capability
Option<A> "value or nothing" pure data, no error info replaces: A | null | undefined
Result<R, L> "success or failure as data" pure data, typed error replaces: { ok: true; value } | { ok: false; error }
Effect<A, E, R> "a program that produces A, may fail with E, needs R" lazy, async, services, retries, tracingPick the lightest type that expresses what you mean. Promote to Effect only when laziness, async, or services enter the picture.
Option.some / Option.none
Creates an Option that contains a value, or one that contains nothing.
import { Option } from "effect"
// ┌─── Option<number>// ▼const found = Option.some(42)const missing = Option.none<number>()
Option.isSome(found) // trueOption.isNone(missing) // trueWhen to Use
When a value might not exist and the caller only needs to distinguish presence from absence, no error information attached.
Option.fromNullishOr
Returns Some(value) when the argument is neither null nor undefined, otherwise None. The bridge between nullable JavaScript APIs and the Option world.
import { Option } from "effect"
const map = new Map<string, string>([["a", "hello"]])
// ┌─── Option<string>// ▼const present = Option.fromNullishOr(map.get("a"))const absent = Option.fromNullishOr(map.get("b"))When to Use
Wrapping Map.get, Array.find, DOM lookups, or any API that signals absence with null/undefined.
Your turn build an `Option.some`, an `Option.none
Option.map / Option.getOrElse / Option.match
Transform a Some without touching None; unwrap with a fallback; or branch on both cases explicitly.
import { Option } from "effect"
const doubled = Option.map(Option.some(5), (n) => n * 2) // Some(10)const skipped = Option.map(Option.none<number>(), (n) => n * 2) // None
const value = Option.getOrElse(Option.none<number>(), () => 0) // 0
const message = Option.match(Option.some({ name: "Alice" }), { onNone: () => "No user", onSome: (u) => `Hello, ${u.name}`,})// "Hello, Alice"Your turn use `Option.map`, `Option.getOrElse`, and `Option.match` to transform and unwrap Options without manual checks.
Result.right / Result.left
Creates an Result carrying a success value (Right) or a failure value (Left).
import { Result } from "effect"
// ┌─── Result<number, string>// ▼const ok = Result.right(42)const err = Result.left("invalid")Details
The success type comes first in Result<R, L>, the opposite of some other libraries. Read it as “value on the right, error on the left”, matching Haskell convention.
When to Use
Synchronous validation, parsing, anything where success and failure are both worth returning as data. If anything async or service-driven enters the picture, promote to Effect.
Your turn wrap a success with `Result.right`, a failure with `Result.left`, then map and match the results.
Yielding Option and Result in Effect.gen
In Effect v4 both types implement the Yieldable protocol. Inside Effect.gen, yield* Option.some(x) unwraps to x; yield* Option.none() short-circuits the effect with a NoSuchElementException.
Result.right(x) unwraps to x; Result.left(e) fails the effect with e.
import { Effect, Option, Data } from "effect"
class NotFoundError extends Data.TaggedError("NotFoundError")<{ readonly id: string}> {}
const findUser = (id: string): Option.Option<{ name: string }> => { const db = new Map([["1", { name: "Alice" }]]) return Option.fromNullishOr(db.get(id))}
// ┌─── Effect<string, NotFoundError, never>// ▼const program = (id: string) => Effect.gen(function* () { const user = yield* findUser(id) return user.name }).pipe(Effect.mapError(() => new NotFoundError({ id })))This pattern bridges synchronous lookups into Effect’s typed-error system without manual Option.match plumbing.
Your turn `yield*` the `Option` directly inside `Effect.gen`, then pipe `Effect.mapError` to translate the default failure into a domain error.
Common Pitfalls
1, Using Option when callers need to know why. None carries no information. If the difference between “not found” and “not permitted” matters, use Result or Effect with a typed error.
2, Result<R, L> parameter order. Success first, error second. Result<number, string> means success is number, error is string.
3, Manually unwrapping Option in Effect.gen. Yield the Option directly; map the NoSuchElementException to a domain error at the pipeline boundary.
4, Reaching for Effect when Option suffices. A pure lookup that returns Option<User> is simpler and faster than Effect<Option<User>>.
When to Use What
| Situation | Use |
|---|---|
| Value might not exist; no error info needed | Option |
| Synchronous validation or parsing | Result |
Map.get, Array.find, optional config |
Option.fromNullishOr |
| Anything async, service-driven, or retryable | Effect |
Bridging an Option into an Effect pipeline |
yield* the Option, then Effect.mapError |
build the `Option` mental model. - [ ] Then `either-constructors` — note that `Result` is right-biased, so `map` walks `R`, not `L`. - [ ] Finish with `yield-in-gen` — confirm that yielding an `Option.None` inside a `gen` block short-circuits the whole effect.
See Also
- Previous,
12-combining-effects: parallel and sequential combinators. - Next,
21-context-basics: dependency injection viaContext. - Related,
04-error-types:Data.TaggedErrorfor structured domain errors. - Related,
09-error-handling: recovering from typed errors.
Run the tests: pnpm test 11-option-either
The full test suite
The index.test.ts for this lesson, verbatim from steps/11-option-either/.
import { describe, expect, it } from "vite-plus/test"import { Data, Effect, Option, Result } from "effect"
class MissingUser extends Data.TaggedError("MissingUser")<{ readonly id: string }> {}
describe("Option and Result", () => { it("uses Option when absence needs no explanation", () => { const value = Option.fromNullishOr("Ada") expect(Option.getOrElse(value, () => "unknown")).toBe("Ada") })
it("uses Result for an already-computed typed failure", () => { const result: Result.Result<string, MissingUser> = Result.fail(new MissingUser({ id: "42" })) expect(Result.isFailure(result)).toBe(true) if (Result.isFailure(result)) expect(result.failure.id).toBe("42") })
it("lifts values into an effectful workflow explicitly", () => { const program = Effect.fromResult(Result.succeed(42)) expect(Effect.runSync(program)).toBe(42) })})