Skip to content

Effect boundaries

The capability probe, the transport contract, and the safety rules the compiler enforces around Effect code.

Updated View as Markdown

The Effect extension activates only when hem.effectImportSource is configured. When this option is omitted, these rules do not apply and the compiler emits no Effect imports.

The preview targets the native effect package at effect@4.0.0, configured as "effectImportSource": "effect". The capability probe below does not certify other versions. See Effect setup.

The capability probe

At build start, the compiler inspects the configured Effect module to verify that it exports the required members.

Namespace Required members
Effect gen, fail, succeed, suspend, all, scoped, tryPromise, fromOption, fromResult, catchTags, catch, fn, fnUntraced, addFinalizer
Layer effect, provide, mergeAll
Data TaggedError
Context Service
Cause UnknownError, NoSuchElementError
Option some, none
Result succeed, fail

The compiler also verifies that Effect.fromOption and Effect.fromResult lift the module’s own Option and Result inside Effect.gen; these are the lifts postfix ! emits for Effect carriers.

If any capability is missing, the compiler reports error HEM3001 and halts the build. Effect types and functions are identified by their module declaration origin rather than their name, preventing conflicts with user-defined types named Effect.

A public conformance suite verifies the probe with runtime fixtures covering finalizer order, partial grouped acquisition, release on interruption, release before catch, boxed Option and Result lifts, and Layer wiring.

Canonical carrier lifts

Standard eager carrier types belong to Hem’s zero-dependency standard library rather than Effect. Inside an effect block, postfix ! adapts them as follows:

Input Success Failure
canonical Result<A, E>! A the original E
canonical Option<A>! A checked NoSuchElementError
native ?A! refused (HEM2027) no error policy exists to invent

The lift is an Effect.suspend that reads the carrier once and returns Effect.fail for the residual or Effect.succeed for the payload; no Hem helper is emitted.

When pattern-matching with let Ok or let Some without an else branch inside an effect block, unmatched values are returned on the success channel, widening the success type rather than failing the effect. Use result! or option! to lift errors into the Effect failure channel, or use an explicit else raise when missing values should become domain errors. Resource cleanup, interruption, error handling, and channel provenance operate normally across adapted values.

Effect’s own eager carriers are Option<A> and Result<A, E>. They are not yieldable inside Effect.gen in this cohort, so postfix ! on a checked Effect Option or Result emits Effect.fromOption or Effect.fromResult. The bare name Result in a .hem file always refers to the canonical type imported from @hem-lang/std/result; Effect’s is written Result.Result<A, E>. Contextual Ok and Err constructors and patterns remain valid on checked Effect Result values and follow its Success/Failure representation.

Provenance

The compiler tracks the success, error, and requirement channels at each !, raise, and await expression. This tracking enables diagnostics to pinpoint the exact line that introduced an error or dependency, rather than only reporting a mismatch at the top-level call site.

For example, if a Users service is missing at Effect.runPromise, the error message links directly to the Users! call that required it. Similarly, unhandled errors link back to the raise statement or operation that added the error type.

Bind and block rules

Code Rule
HEM2004 ! appears outside an effect block
HEM4103 ! crosses an arrow or function boundary; the fix names Effect.forEach
HEM2009 A nested bind inside a group member, which would silently serialize
HEM4105 A Scope-carrying effect is bound with ! instead of use; the fix rewrites let lease = acquire()! to use lease = acquire(), keeping the authored name
HEM4114 A traced operation is bound somewhere without stable trace identity

effect traced requires one named, immutable binding with a block body. Local bindings may capture dependencies. Names include the module and enclosing named bindings; collisions receive a source-location suffix. Executions of one definition share a name. HEM4114 still rejects mutable, grouped, destructured, wildcard, or anonymous definitions. Each invocation owns its cleanup; capturing a service does not extend its lifetime or let ordinary callbacks borrow the operation’s bind boundary.

Execution and wiring rules

Code Rule
HEM4102 Requirements were never provided; blame points at the sites that introduced them
HEM4106 An Effect.run* call appears inside an Effect rather than at an application edge
HEM4107 More than one provide boundary in a single lifecycle
HEM4108 A Layer.mergeAll whose members depend on one another
HEM4109 A service implementation leaks its own construction requirements

These rules enforce consistent runtime lifecycles. Calling an Effect.run* runner inside an effect drops the parent supervision, contextual services, test clock, interruption scope, and tracing context. Multiple provide boundaries can cause layers to be constructed multiple times. Merging interdependent layers with Layer.mergeAll loses dependency ordering. Hem flags these patterns at compile time while allowing explicit Effect library calls when separate lifecycles are required.

Channel safety rules

Code Rule
HEM4110 An exported Effect boundary has an any or unknown error or requirement channel
HEM4111 An assertion narrows away success, failure, or service members
HEM4115 A Promise or PromiseLike is hidden in the success channel of Effect.succeed, Effect.sync, or another Effect-returning call
HEM4118 An Effect is nested in a generator return, a success channel, or a failure channel
HEM4119 for await appears inside an Effect block, where await lifts a single Promise

When Hem’s only errors are any/unknown consequences, TypeScript errors are still reported first; HEM4110 is withheld when a TypeScript error lies inside its boundary span in the same file.

Assertions between Effect types may widen channels, but cannot narrow them (HEM4111). An unknown value cannot be asserted to a concrete Effect type, including at an import boundary or after passing through an alias or helper (HEM2028). Use a correctly typed library contract or construct an Effect from checked data. An unchecked local TypeScript shim requires a separate boundary review; it is not proof supplied by Hem.

The same rule applies through unknown-returning helpers, generic identity helpers, and capturing closures. Authored predicate and assertion signatures cannot supply the missing evidence (HEM2029); imported signatures remain trusted foreign contracts. A checked widening preserves the original Effect object. An empty typed-error channel does not rule out defects or interruption.

Transfers between configured Effect types also check write access in their success, error, and requirement values (HEM2030). A wider Effect view cannot make a readonly payload writable. Effect’s internal variance markers remain part of the trusted library contract, not application records checked by Hem.

HEM4115 prevents wrapping a Promise in Effect.succeed or Effect.sync, where Effect cannot manage its cancellation, retries, or errors. Asynchronous operations must enter through Hem’s await inside an effect block or through Effect.tryPromise.

HEM4118 prevents returning an unexecuted Effect value inside another Effect’s success channel. Use postfix ! to execute and bind nested effects.

Resource rules

Code Rule
HEM4112 defer outside an Effect scope
HEM4113 A cleanup value whose type does not satisfy the contract

defer accepts an Effect<void, never, R>. Synchronous cleanup functions must be lifted explicitly, return values must be discarded, and errors must be handled before registration. Exit-sensitive cleanups use Effect.addFinalizer.

Finalizers run on success, failure, defect, and interruption in last-in, first-out (LIFO) order. Scopes close before any outer catch block runs, ensuring resources are released before recovery logic executes.

Installation identity rules

Code Rule
HEM4116 Resolved declarations come from more than one physical effect installation
HEM4117 A generic class directly or transitively inherits the configured Context.Service

HEM4116 triggers when multiple physical installations of effect are found, even if their version numbers match. The compiler compares canonical filesystem paths, so symlinks to the same installation are treated as one.

HEM4117 prohibits generic service classes because TypeScript type arguments are erased at runtime; for example, Cache<User> and Cache<Order> would share the same runtime tag key. Generic methods on non-generic services remain supported.

What remains ordinary Effect

Hem provides dedicated syntax for common control flow patterns, while runtime policies remain explicit Effect API calls. Scheduling and retries, collection concurrency, fibers, streams, layer configuration, exit-sensitive finalizers, and cancellation adapters use library functions. Follow the API contract of the pinned runtime: documentation for another Effect version may use different names or signatures. Effect tooling must also be compatible with that version and the generated code; the compiler probe does not certify third-party tools.

Normative source

Specification 0.2, §14 (the Effect extension) and §10 (canonical carriers). For the rationale behind capability probing, see why Effect, and why Effect in Hem.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close