Optional extension
Direct syntax and compiler checks for Effect.
Hem doesn't need Effect, but works with it. With the extension on, Effect keeps its runtime, ecosystem, and three-channel type; Hem provides direct syntax for common application patterns, emits readable Effect TypeScript, and rejects mistakes that ordinary TypeScript accepts.
The extension is optional. Point effectImportSource at a compatible transport to enable it. Omit the option and core Hem has no Effect import or runtime. This preview targets the native effect package ateffect@4.0.0; the generated code is ordinary Effect 4 TypeScript with no Hem runtime. It does not support Effect 3 or other Effect 4 versions. See the setup and support contract before adopting it.
{ "hem": { "effectImportSource": "effect" }}domain/users.hem
domain/users.tsemitted
Compiler knowledge
The compiler analyzes the Effect program.
Beyond concise syntax, Hem records where services and failures enter, checks asynchronous execution and resource ownership, and reports the error on the Hem line that introduced it.
- Success, error, and requirement provenance stays attached to the authored bind
- Scope, runner, Layer, package-identity, Promise, and nested-Effect mistakes are checked together
- Policy-bearing APIs such as retry, Schedule, Stream, and Layer construction remain visible Effect calls
error[HEM4102] routes/users.hem:18:42
18getUser(id) |> Effect.runPromise
| ^^^^^^^^^^^^^^^^^Provide the required service before running this Effect: `Users`.
= hint: `Users` entered at `let users = Users!` in domain/users.hem:9
error[HEM4115] providers/llm.hem:7:29
7let answer = Effect.succeed(client.complete(prompt))
| ^^^^^^^^^^^^^^^^^^^^^^^`Effect.succeed` leaves `Promise<Answer>` in the success channel, so Effect does not own the asynchronous operation.
= hint: Lift it with `await` inside an effect block or `Effect.tryPromise`.
Binding
Preserve Effect semantics without generator syntax.
An
effect block evaluates to a description of work. Nothing runs until an Effect.run* at the edge, so binding a block to a name and using it twice runs it twice. Postfix ! binds anythingEffect.gen accepts.returncompletes the block,raisefails the error channel,throwis a defectawaitinside a block lifts a promise throughEffect.tryPromise- A bare effect statement is rejected; bind it or discard it explicitly
error[HEM4103] sync.hem:12:47
12let results = ids.map((id) => fetchUser(id)!)
| ^Postfix `!` cannot bind through an arrow or function boundary. Return the effect from the callback and bind the collection operation with `Effect.forEach`.
let res = await fetch(url) catch { _ => raise NetworkDown({ url }) }let data = await res.json() catch { _ => raise BadPayload({ status: res.status }) }Failures
Failures get a typed channel.
error, raise, andcatch are the same constructs from Part I. Under the extension,raise routes errors through the typed channel instead of the call stack, and tsc tracks the remaining error types.- Tag arms lower to
Effect.catchTags, so the error channel shrinks - Declarations emit
Data.TaggedError, shape-compatible with the core emit throwis a defect and sails past_by design
let user = getUser(id) catch {| UserNotFound(_) => anonymousUser| DbError(e) => effect { Log.warn(e)!; raise ServiceDown }}Services
Typed dependency injection.
A
service declaration is an interface your domain depends on, emitted as a Context.Service class. The edge of an app is one pipe chain that provides the layer and runs the effect.Users!yields the live implementation- Layers stay ordinary library code, written once per service
- Forget one and the error names the service and where it entered
let UsersLive = Layer.effect(Users, effect { let db = Db! makeUsersService(db)})export let loader = ({ params }: Route.LoaderArgs) => getUser(params.userId) |> Effect.provide(AppLayer) |> Effect.runPromiseResources
Releases survive interruption.
use binds an effect whose requirements include a scope, and registers its release into the enclosing block. That is the part plain JavaScript can't express, since a cancelled fiber still runs its finalizers.- The block is the scope, so a smaller block is a shorter lifetime
- Finalizers run on success, failure, or interruption, LIFO
- The same
useoutside a block lowers to the platform'susing
report.hem
report.tsemitted
Groups
Structured concurrency.
The
and group is the same one that works over plain promises. Over Effect the first failure interrupts its in-flight siblings, which promises can't do because they can't be cancelled.- Members may not reference each other; that check is HEM2007
- Adding or deleting an
andis the whole refactor use x = … and y = …acquires concurrently into the block scope
dashboard.hem
dashboard.tsemitted
Adopting
Adopt it one module at a time.
Hem modules import ordinary TypeScript and the full Effect API. TypeScript modules import the generated files. Start with one operation, inspect the output, and keep library code explicit wherever retry, scheduling, streaming, Layer architecture, or cancellation policy requires an explicit decision.