---
title: "effect { } — a description, not an action"
image: "https://hem.run/social/hem.png"
---

> Documentation Index
> Fetch the complete documentation index at: https://hem.run/llms.txt
> Use this file to discover all available pages before exploring further.

# effect { } — a description, not an action

An `effect { }` block is Hem's one lazy construct: it evaluates to a _description_ of work. Nothing runs until an `Effect.run*` at the edge of your program: bind a block to a name and use it twice, it runs twice.

```hem title="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>
```

```ts emitted title="users.ts — generated"
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;
  });
```

A bare effect statement is refused (HEM2018): `users.findById(id)` with no `!` inside a block describes work and then drops the description, so the message points at the `!` you meant. Run it with `!`, or (when you truly mean to ignore the result) write `let _ = users.findById(id)!`.

Inside a block, three exits mean three things: **`return`** completes the block early with a success value (the block, not the enclosing function; a block is its own generator), **`raise`** fails the typed error channel, and **`throw`** is a defect: an unrecoverable bug, per Effect's semantics. `raise` diverges, so it slots into `let`-`else` as the guard idiom above. And `await` changes transport inside a block: it lifts a promise-typed expression through `Effect.tryPromise`, thunked so retries re-execute it. Like `!`, these block meanings stop at a function boundary: inside an arrow nested in the block, `await` is plain JavaScript `await` (so an `Effect.tryPromise(async () => ...)` callback reads as written) and `raise` is the core `throw`, which reaches the block as a defect if nothing catches it. Uncaught rejections surface as the library's `Cause.UnknownError`; `catch` is the same handler you met in §11: arms bind the raw thrown value, recover, and anything unmatched travels on. Translating into your typed domain is just an arm that raises:

```hem
let res  = await fetch(url) catch { _ => raise NetworkDown({ url }) }
let data = await res.json() catch { _ => raise BadPayload({ status: res.status }) }
```

Bare promises never bind on their own (the lift is always spelled), and abort propagation to cancellable APIs uses the explicit `Effect.tryPromise((signal) => fetch(url, { signal }))!` form for now.

That ownership rule also applies when calling Effect directly. `Effect.succeed(Promise.resolve(value))` is HEM4115: the effect succeeds immediately with a Promise value, so Effect cannot await, retry, interrupt, or translate its rejection. Use `await` in an Effect block, or construct the operation with `Effect.tryPromise`.

Hem also rejects duplicate `effect` installations, generic service tags whose erased type arguments would collide at runtime, and Effects carried as success or failure data. These are Hem's checked application boundaries, not a claim that every refused shape is invalid TypeScript. The [Effect boundaries reference](https://hem.run/reference/effect-boundaries) explains them and the probe contract.

`!` is the bind operator. It yields anything `Effect.gen` accepts and is postfix-tight, so piping before binding requires parentheses: `(getUser(id) |> Effect.timeout("5 seconds"))!`. The compiler enforces that `!` cannot bind across function boundaries:

```
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`.
```

Name the operations that deserve tracing explicitly:

```hem title="users.hem"
export let fetchUser = effect traced (id: UserId): Effect<User, UserNotFound, Users> => {
  let users = Users!
  users.findById(id)!
}
```

```ts emitted title="users.ts — generated"
export const fetchUser = Effect.fn("src/domain/users#fetchUser")(
  function* (id: UserId): Effect.fn.Return<User, UserNotFound, Users> {
const users = yield* Users;
return yield* users.findById(id);
  },
);
```

Hem stays arrow-only: there is no `effect fn`. The `effect traced` prefix requires one immutable named binding with a block body. Local operations may capture dependencies; their trace names include enclosing named bindings, with a source-location suffix for collisions. Defining an operation does not execute it or extend the lifetime of captured services. Cleanup belongs to the operation's execution. The ordinary `(id) => effect { ... }` form remains untraced and keeps its existing `Effect.gen` emit.

```hem
let makeLookup = (prefix: string) => {
  let lookup = effect traced (id: string): Effect<string> => {
`${prefix}${id}`
  }
  lookup
}
let lookup = makeLookup("user:")
let program = effect { lookup("42")! }
```

Source: https://hem.run/tour/effect-a-description-not-an-action/index.mdx
