02·01Option & 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.

11 - Option and Result

Previous · Next

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, tracing

Pick 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) // true
Option.isNone(missing) // true

When 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()`, and use `Option.fromNullishOr` to bridge nullable values.


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 via Context.
  • Related, 04-error-types: Data.TaggedError for 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/.

index.test.ts
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)
})
})
← PreviousError AccumulationNext →Combining Effects