Skip to content

Standard Option and Result

The canonical boxed carrier ABI, APIs, propagation rules, and explicit Effect bridge.

Updated View as Markdown

Hem ships eager Option<T> and Result<T, E> values independently of Effect. They live in the zero-dependency @hem-lang/std runtime and are always explicit imports:

import { Option } from "@hem-lang/std/option"
import { Result } from "@hem-lang/std/result"

There is no importable @hem-lang/std root. Each focused subpath exports one same-named type/value pair and no standalone aliases.

Choose the representation deliberately

Need Representation
Ordinary platform-shaped absence without allocation ?T
Nested absence, combinators, a stable tag, or must-use signaling Option<T>
Expected recoverable failure with a typed payload Result<T, E>
Lazy work, interruption, resources, services, or concurrency Effect<A, E, R>

Some allocates a wrapper. That cost is intentional and observable. Use ?T on hot or platform-shaped paths where an explicit box provides no advantage.

Frozen JavaScript ABI

export type Option<T> =
  | { readonly _tag: "Some"; readonly value: T }
  | { readonly _tag: "None" };

export type Result<T, E> =
  | { readonly _tag: "Ok"; readonly value: T }
  | { readonly _tag: "Err"; readonly error: E };

These are transparent, serializable, forgeable tagged objects—not classes or Effect values. Match on _tag; wrapper reference identity is not semantic equality. Err names recoverable failure data. Error remains the JavaScript/Hem thrown-error class concept.

Qualified constructors keep the absent branch precise:

Option.some<A>(value: A): Option<A>
Option.none: Option<never>
Result.ok<A>(value: A): Result<A, never>
Result.err<E>(error: E): Result<never, E>

The never branch lets TypeScript infer exact error unions through composition.

Contextual constructors and patterns

Capitalized Some/None and Ok/Err are contextual syntax. A checked expected type selects construction; a checked scrutinee selects matching.

let port: Option<number> = Some(8080)
let parsed: Result<number, string> = Ok(8080)

let show = (value: Result<number, string>): string => match value {
  | Ok(number) => number.toString()
  | Err(message) => message
}

Without an expected carrier, Some(value) or Ok(value) refuses with HEM2025. Use Option.some(value) or Result.ok(value) to establish the type. Recognition follows the resolved standard declaration, not a type name or structural resemblance.

Carrier binding

An immutable root Some or Ok binding with no else propagates the original residual to the nearest arrow, async arrow, or Effect success-channel boundary:

let decode = (text: string): Result<User, DecodeError> => {
  let Ok(fields) = parseFields(text)
  Result.ok(User.fromFields(fields))
}

The RHS evaluates once. Success binds the payload (.value on Ok and Some); failure returns the original Err or None object — an Err carries its payload as .error, so unnarrowed payload access on a Result is a type error. Native ?T uses the same Some spelling and returns the exact observed null or undefined sentinel.

The rules are deliberately narrow:

  • source and destination use the same carrier representation;
  • the nearest arrow, async arrow, or Effect block is the boundary, so a nested callback cannot return from its caller;
  • only root Some and Ok propagate—None and Err never invert success;
  • the payload below the carrier root must be statically irrefutable;
  • an explicit else always selects ordinary let–else behavior;
  • canonical boxed carriers are must-use, while native ?T is not generally must-use.

Async transport keeps await explicit:

let load = async (): Promise<Result<User, LoadError>> => {
  let Ok(user) = await fetchUser()
  Result.ok(user)
}

The Promise resolves before carrier inspection. A resolved residual returns through the async function; rejection remains rejection. A missing await, incompatible destination, nested refutation, or invalid boundary reports HEM2026 at authored Hem.

Option API

All operations are synchronous, fixed-arity, uncurried, and data-first.

Operation Contract
some, none Construct a present or absent boxed value
isSome, isNone Narrow by the public tag
map, flatMap, flatten, filter Transform or select present values
getOr, getOrElse Eager or lazy value fallback
orElse Lazy alternative Option
fromNullish, toNullish Explicit boxed/native border; None unboxes to undefined
toResult Convert None with a lazy error producer
all Combine already-produced values in input order

Result API

Operation Contract
ok, err Construct success or recoverable failure
isOk, isErr Narrow by the public tag
map, mapError, flatMap, flatten Transform success or error channels
getOr, getOrElse Eager or lazy success fallback
orElse Recover from Err, receiving its error payload
toOption Deliberately discard the error
all Combine already-produced values and union heterogeneous errors

Callbacks are called only on their documented branch. A callback throw escapes unchanged; the combinators never turn defects into Err. all([first(), second()]) sees both calls only after JavaScript has evaluated the array elements. It scans left-to-right and returns the first residual object, but it does not start, cancel, or schedule work.

The pipe inserts into the first argument, so piped and direct forms have the same call ABI:

let normalized = option |> Option.map(normalize)
// Option.map(option, normalize)

Use an explicit arrow to pipe into a later argument.

Effect interoperability

Inside a configured effect or resource block, postfix ! is the explicit adapter:

let program = effect {
  let user = lookupResult()!
  let token = lookupOption()!
  render(user, token)
}

Canonical Result<T, E>! yields T or fails with the original E. Canonical Option<T>! yields T or the configured transport’s checked NoSuchElementError. Native ?T! reports HEM2027 because absence has no error payload or policy. An omitted-else carrier binding inside Effect propagates the unchanged residual through the Effect success channel. Use postfix ! instead when the carrier residual should become an Effect failure.

Effect’s eager failure carrier is Result<A, E>, written Result.Result<A, E> through its namespace; postfix ! on it emits Effect.fromResult. The pre-release bare Effect Result<T, E> alias is removed, so a bare Result has one meaning: the canonical carrier.

Foreign carriers

better-result, local variants, and structurally identical TypeScript unions remain ordinary dependencies. Hem does not vendor them, detect them structurally, or grant them carrier binding or must-use behavior. Convert explicitly at the integration seam by matching the foreign value and calling Option.some/none or Result.ok/err.

Before granting canonical semantics, the compiler probes the resolved standard declaration contract. An unsupported package fails once at the project boundary with HEM3006 instead of causing generated TypeScript errors.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close