Hem uses a single binding-group syntax for independent work. Group members joined with and are evaluated independently; no member can reference a name bound by another member in the same group. The marker on each member specifies the transport.
and groups
Pure groups
A plain let group evaluates initializers from left to right, then binds the names. All values are computed before the bindings become available, matching the structure of carrier groups without introducing asynchronous execution.
let first = readFirst()
and second = readSecond()
let total = first + secondThe emitted TypeScript consists of standard sequential bindings. A group does not allow referencing an earlier member; split dependent operations into separate sequential bindings.
Awaited groups
When every member is marked with await, Hem runs all promise expressions concurrently using Promise.all and binds the results in declaration order. Destructured members use a tuple result; identifier members use positional mapping.
let user = await fetchUser(id)
and settings = await fetchSettings(id)
show(user, settings)If a promise rejects, the entire group rejects. Because JavaScript promises do not provide a general cancellation mechanism, other in-flight operations continue running unless the underlying APIs implement cancellation. Use an explicit cancellable API when an operation must observe an AbortSignal.
Effect groups
Inside an effect { } block, every member is bound using postfix !. Hem compiles the group to Effect.all with unbounded concurrency. If any member fails, in-flight sibling fibers are interrupted immediately so failing operations do not continue running in the background.
let dashboard = (id: string) => effect {
let user = fetchUser(id)!
and preferences = fetchPreferences(id)!
render(user, preferences)
}Effect cancellation is structured: interruption propagates to active child fibers in the group. This distinguishes Effect groups from awaited Promise groups; Hem syntax remains identical except for the marker and enclosing block type.
Carrier groups
Canonical Option and Result bindings follow the same grouping rules. All member expressions evaluate first. After evaluation, Hem inspects members in declaration order; the first None or Err residual propagates unchanged to the nearest arrow, async arrow, or Effect success-channel boundary. Success values are bound normally.
let load = (id: string): Result<{ user: User, theme: string }, LoadError> => {
let Ok(user) = loadUser(id)
and Ok(settings) = loadSettings(id)
Result.ok({ user: user, theme: settings.theme })
}An awaited carrier group requires await on every member and runs via Promise.all before checking residuals. Carrier groups do not introduce cancellation; behavior depends on whether the underlying transport is a Promise or an Effect.
Scope-safe acquisition groups
With the Effect extension enabled, use bindings can form a group with and. Acquisitions run concurrently and register finalizers in the enclosing scope. If one acquisition fails, the scope releases previously acquired resources, ensuring safe concurrent acquisition.
Core use groups are rejected. Core disposal relies on JavaScript’s lexical using scope, which cannot handle interruption or clean up partially completed concurrent acquisitions.
let report = (sql: string) => effect {
use db = Db.acquire
and cache = Cache.acquire
let rows = db.query(sql)!
renderRows(rows, cache)
}Markers within a group must be uniform: all pure, all awaited, all Effect-bound with !, or all extension use bindings. Mixing let and use is prohibited, and nested ! inside a member is rejected because it would serialize execution.
Dependent work
Hem rejects sibling references within the same group. Move dependent computations to a subsequent sequential binding, or extract the dependent logic into a sub-flow and treat that sub-flow as one member of the group.
let load = (id: string) => effect {
let user = fetchUser(id)!
and orgs = fetchOrgs(user.orgId)!
{ user, orgs }
}Diagnostic HEM2007 indicates a sibling binding reference. To handle dependent Effect graphs, use separate let bindings, destructure a named sub-flow, use Effect.fork and Fiber.join, or use Effect.forEach with explicit concurrency settings. Plain async code uses an async helper or platform promise and cancellation APIs.
Resource ownership
Core use
use pat = expression requires a value implementing Disposable or AsyncDisposable. In core Hem, this compiles to JavaScript’s using or await using based on the resource type. Resources are released in last-in-first-out (LIFO) order when the enclosing block exits, including on thrown errors. Core disposal has no interruption model and relies on the host runtime’s disposal protocol.
let readReport = async () => {
use file = await openReport()
await file.read()
}Limit the scope of use to the resource’s active lifetime. Binding a non-disposable value produces type error HEM2012; use an adapter if a library does not implement the standard disposal protocol.
Effect use
Inside an effect { } block, use binds an Effect requiring a Scope. The enclosing effect block serves as the scope: Hem wraps blocks containing scoped acquisitions in Effect.scoped. Registered finalizers run in LIFO order on success, typed failure, defect, or interruption. The scope wrapper sits inside any postfix catch, ensuring cleanup finishes before error recovery executes.
let report = (sql: string) => effect {
use db = Db.acquire
let rows = db.query(sql)!
renderRows(rows)
}Binding a Scope-requiring Effect with ! is rejected (HEM4105); use is required to transfer ownership explicitly.
resource { }
resource { } follows the same acquisition and cleanup semantics as effect { }, but leaves the scope open. Scope remains in the returned Effect’s requirement channel, and any use or defer statements register into the caller’s scope. Use resource { } when creating services or layers whose lifetime is managed by the caller.
let DatabaseLive = Layer.effect(Database, resource {
use db = Db.acquire
asDatabase(db)
})The block type determines ownership: effect { } closes its scope, while resource { } passes the scope to the caller.
defer
defer cleanup registers an infallible Effect<void, never, R> without binding a value. Wrap synchronous cleanup functions with Effect.sync, explicitly discard return values, and handle typed errors before registration.
let serve = effect {
defer Effect.sync(() => instruction.clear(id))
work()!
}defer does not accept an exit value. For exit-aware cleanup, call Effect.addFinalizer directly. Both use and defer execute in reverse registration order on any Effect exit, including interruption.
Related diagnostics
| Code | Meaning |
|---|---|
| HEM2007 | A group member references a sibling binding. |
| HEM2008 | Group markers or binder kinds are mixed, or core use appears in a group. |
| HEM2009 | A group member contains a nested bind that would serialize work. |
| HEM2012 | use needs a disposable resource in core. |
| HEM4105 | A Scope-carrying Effect was bound with ! instead of use. |
| HEM4112 | defer appears outside an Effect scope. |
| HEM4113 | A deferred cleanup does not satisfy the cleanup contract. |
Normative source
Specification 0.2, §4 (bindings), §9 (errors and exceptions), and §14.6–§14.7 (Effect resources and concurrency). The tour’s worked forms are in sections 20 and 21.