Skip to content
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.

tsconfig.jsonjson
{  "hem": { "effectImportSource": "effect" }}
domain/users.hem
error UserNotFound({ id: string })service Users {  findById: (id: string) => Effect<?UserRow, DbError>}export let getUser = (id: string) => effect {  let users = Users!  let row = users.findById(id)!  let Some(user) = row else raise UserNotFound({ id })  user}// tsc infers: (id: string) => Effect<UserRow, DbError | UserNotFound, Users>
domain/users.tsemitted11 lines in, 21 out
import { Context, Data, Effect } from "effect";class UserNotFound extends Data.TaggedError("UserNotFound")<{  readonly id: string;}> {}class Users extends Context.Service<  Users,  {    readonly findById: (id: string) => Effect.Effect<UserRow | null | undefined, DbError>;  }>()("src/domain/users#Users") {}export const getUser = (id: string) =>  Effect.gen(function* () {    const users = yield* Users;    const row = yield* users.findById(id);    const _hemValue = row;    if (_hemValue === null || _hemValue === undefined) {      return yield* Effect.fail(new UserNotFound({ id }));    }    const user = _hemValue;    return user;  });
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
|
18 | getUser(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
|
7 | let 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.
  • return completes the block, raise fails the error channel, throw is a defect
  • await inside a block lifts a promise through Effect.tryPromise
  • A bare effect statement is rejected; bind it or discard it explicitly
error[HEM4103] sync.hem:12:47
|
12 | let 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`.
edge.hemhem
let res  = await fetch(url) catch { _ => raise NetworkDown({ url }) }let data = await res.json() catch { _ => raise BadPayload({ status: res.status }) }
Read the chapter: effect { }: a description, not an action
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
  • throw is a defect and sails past _ by design
users.hemhem
let user = getUser(id) catch {| UserNotFound(_) => anonymousUser| DbError(e) => effect { Log.warn(e)!; raise ServiceDown }}
Read the chapter: Typed errors and catch
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
app.hemhem
let UsersLive = Layer.effect(Users, effect {  let db = Db!  makeUsersService(db)})export let loader = ({ params }: Route.LoaderArgs) =>  getUser(params.userId) |> Effect.provide(AppLayer) |> Effect.runPromise
Read the chapter: Services and the edge
Resources

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 use outside a block lowers to the platform's using
report.hem
export let report = (userId: string) => effect {  use conn = Db.acquire  let rows = conn.query(sql)!  render(rows)}
report.tsemitted
export const report = (userId: string) =>  Effect.scoped(    Effect.gen(function* () {      const conn = yield* Db.acquire;      const rows = yield* conn.query(sql);      return render(rows);    }),  );
Read the chapter: Resources: use and resource { }
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 and is the whole refactor
  • use x = … and y = … acquires concurrently into the block scope
dashboard.hem
let dashboard = (id: string) => effect {  let user  = fetchUser(id)!  and orgs  = fetchOrgs(id)!  and prefs = fetchPrefs(id)!  render(user, orgs, prefs)}
dashboard.tsemitted
const dashboard = (id: string) => Effect.gen(function* () {  const { user, orgs, prefs } = yield* Effect.all(    { user: fetchUser(id), orgs: fetchOrgs(id), prefs: fetchPrefs(id) },    { concurrency: "unbounded" },  );  return render(user, orgs, prefs);});
Read the chapter: Concurrency: and groups
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.
Navigation

Type to search…

↑↓ navigate↵ selectEsc close