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.