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
SomeandOkpropagate—NoneandErrnever invert success; - the payload below the carrier root must be statically irrefutable;
- an explicit
elsealways selects ordinarylet–elsebehavior; - canonical boxed carriers are must-use, while native
?Tis 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.
Related reference
- Data transformation explains first-argument pipes.
- Absence covers native nullish values and exact foreign contracts.
- Typed failures covers the optional Effect failure boundary.