?T remains the allocation-free choice for everyday absence. When nesting, combinators, a stable
serialized tag, or a visible recoverable failure matters, import Hem’s canonical boxed carriers:
import { Option } from "@hem-lang/std/option"
import { Result } from "@hem-lang/std/result"
let port: Option<number> = Some(8080)
let parsed: Result<number, string> = Ok(8080)The checked type gives Some/None and Ok/Err their representation. Without that context,
write Option.some(8080) or Result.ok(8080) so inference starts from Option<number> or
Result<number, never>. Both values are transparent readonly objects with _tag and .value, and
both are must-use even in a project that never enables Effect. let _ = operation() is the one
deliberate discard.
The APIs are synchronous, fixed-arity, and data-first. That makes direct and piped code one call:
let normalized = port |> Option.map((value) => value + 1)
let recovered = parsed |> Result.getOr(3000)The most important operation is syntax rather than a generator helper. An omitted-else success
binding returns the original residual from the nearest arrow:
let decodePort = (text: string): Result<number, ParseError> => {
let Ok(value) = parsePort(text)
Result.ok(value)
}
let loadPort = async (): Promise<Result<number, LoadError | ParseError>> => {
let Ok(text) = await readPortText()
let Ok(port) = parsePort(text)
Result.ok(port)
}The RHS runs once. Err is returned unchanged; Some similarly propagates boxed None, while a
native ?T binding preserves the exact observed null or undefined. The destination must use
the same representation, callbacks form their own boundary, and Promise rejection stays rejection.
Adding else always means ordinary local recovery. Inside effect {} the block itself is the
boundary: the residual returns unchanged on the Effect success channel, and only postfix ! moves
a failure into the Effect failure channel.
Carrier bindings also group with and. Every member evaluates to completion — sequentially when
pure, under one Promise.all when awaited, under one Effect.all when bang-marked — and then the
members are checked in the order you wrote them; the first residual returns unchanged:
let loadPair = async (): Promise<Result<Pair, LoadError>> => {
let Ok(left) = await readLeft() and Ok(right) = await readRight()
Result.ok(Pair(left, right))
}Effect remains a separate lazy transport, using postfix ! to bridge carriers into effects:
let program = effect {
let port = parsePort(rawPort)!
let token = Option.fromNullish(env.TOKEN)!
connect(port, token)!
}Canonical Err<E> enters the Effect error channel as that exact E; canonical None becomes the
transport’s checked NoSuchElementError. Native ?T! is refused because nullish absence carries no
error policy. Effect 4’s own eager carriers are Option and Result<A, E> (spelled
Result.Result through its namespace; the pre-release bare Result alias is gone). Postfix ! on
them lowers to Effect.fromOption/Effect.fromResult, and checked Effect Result values still
construct and match with Ok and Err.
This provides a consistent model: focused package imports at the boundary, ordinary sync and async carrier propagation in domain code, and one explicit adapter where the program enters Effect. The carrier reference lists every operation, the frozen ABI, and third-party interoperability rules.