---
title: "Why Effect, and why Effect in Hem"
description: "Two arguments with worked comparisons: that Effect's model earns its place for real services, and that its notation is the part Hem removes."
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.

# Why Effect, and why Effect in Hem

This page compares plain TypeScript, Effect, and Hem across five common service scenarios.

First, for services with retries, timeouts, fan-out, resource management, and dependency injection, Effect provides a more reliable model than plain promises and manual conventions.

Second, Hem retains that model while providing dedicated language syntax and additional compiler checks for common patterns.

Each scenario below shows three implementations: plain TypeScript, raw Effect, and Hem. The examples model a Cloudflare Workers application calling remote LLM APIs.

Hem is an application language, not a runtime replacement. Emitted code uses Effect, and complex policies use standard Effect functions.

The five sections cover typed errors, resilience policies, cancellable concurrency, automatic resource management, and swappable dependencies.

## 1. Errors are part of the type

To determine what a plain TypeScript function throws, you must inspect its implementation and all called dependencies.

```ts title="Plain TypeScript"
async function getScan(domain: string): Promise<Scan> {
  const record = await findScan(domain); // throws? returns null? both?
  if (!record) throw new NotFoundError(domain);
  const whois = await fetchWhois(domain); // throws on 4xx? 5xx? timeout?
  return buildScan(record, whois);
}

try {
  return await getScan(domain);
} catch (e) {
  // e: unknown. An instanceof ladder, and hope you got them all.
}
```

TypeScript does not track error types in `Promise` signatures. If a function deeper in the call stack adds a new failure mode, the type checker will not flag unhandled cases.

In Effect, failure types appear in the second type parameter and are inferred automatically:

```ts title="Raw Effect"
class ScanNotFound extends Data.TaggedError("ScanNotFound")<{ readonly domain: string }> {}
class WhoisUnavailable extends Data.TaggedError("WhoisUnavailable")<{ readonly status: number }> {}

const getScan = (domain: string) =>
  Effect.gen(function* () {
const record = yield* findScan(domain);
if (record == null) return yield* Effect.fail(new ScanNotFound({ domain }));
const whois = yield* fetchWhois(domain);
return buildScan(record, whois);
  });
// inferred: Effect<Scan, DbFailed | ScanNotFound | WhoisUnavailable, never>
```

Handling an error removes it from the error channel type. A dependency's new failure can widen and
propagate through compatible Effect error channels; TypeScript reports the incompatible boundaries
that must handle it or widen their declared contract.

Hem keeps these semantics with shorter syntax:

```hem title="Hem"
error ScanNotFound({ domain: string })
error WhoisUnavailable({ status: number })

let getScan = (domain: string) => effect {
  let Some(record) = findScan(domain)! else raise ScanNotFound({ domain })
  buildScan(record, fetchWhois(domain)!)
}

let scan = getScan(domain) catch { ScanNotFound(_) => freshScan(domain) }
```

Error types are declared in a single line. Guard clauses replace explicit null checks and failures. Pattern-matching `catch` blocks match against the inferred error channel; if a pattern does not match an actual error type, the compiler reports an error.

## 2. Resilience is a value

Consider calling an API with a 30-second timeout, retrying on HTTP 429 and 5xx responses with jittered exponential backoff for up to five attempts.

In plain TypeScript:

```ts title="Plain TypeScript"
async function askModel(prompt: string): Promise<Answer> {
  let delay = 250;
  for (let attempt = 1; ; attempt++) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 30_000);
try {
  const res = await fetch(MISTRAL_URL, { ...request(prompt), signal: controller.signal });
  if (res.status === 429 || res.status >= 500) throw new Retryable(res.status);
  if (!res.ok) throw new UpstreamFailed(res.status);
  return parseAnswer(await res.json());
} catch (e) {
  const retryable =
    e instanceof Retryable || (e instanceof DOMException && e.name === "AbortError");
  if (!retryable || attempt >= 5) throw e;
  await new Promise((r) => setTimeout(r, delay + Math.random() * delay));
  delay *= 2;
} finally {
  clearTimeout(timer);
}
  }
}
```

This implementation mixes the retry policy into the request logic. Reusing this policy requires duplicating code or writing custom abstractions. Error types remain untracked, cancellation requires manual `AbortController` plumbing, and testing backoff requires timer mocks.

In Effect, retry and timeout policies are standalone values that can be reused and tested independently:

```ts title="Raw Effect"
const backoff = Schedule.exponential("250 millis").pipe(Schedule.jittered);
const flaky = (e: AskError) =>
  e._tag === "RateLimited" || (e._tag === "UpstreamFailed" && e.status >= 500);

const askModel = (prompt: string) =>
  callMistral(prompt).pipe(
Effect.timeout("30 seconds"),
Effect.retry({ schedule: backoff, times: 4, while: flaky }),
  );
```

In Hem, including the underlying HTTP request:

```hem title="Hem"
error RateLimited
error UpstreamFailed({ status: number })

let backoff = Schedule.exponential("250 millis") |> Schedule.jittered
let flaky = (e: RateLimited | UpstreamFailed | TimeoutError) => match e {
| RateLimited => true
| UpstreamFailed({ status }) => status >= 500
| _ => false
}

let askModel = (prompt: string) =>
  effect {
let res = Effect.tryPromise({
  try: (signal) => fetch(mistralUrl, { ...request(prompt), signal }),
  catch: (_) => new UpstreamFailed({ status: 0 }),
})!
if res.status == 429 { raise RateLimited() }
if !res.ok { raise UpstreamFailed({ status: res.status }) }
let json = await res.json() catch { _ => raise UpstreamFailed({ status: res.status }) }
match Answer.safeParse(json) {
| { success: true, data } => data
| _ => raise UpstreamFailed({ status: res.status })
}
  }
  |> Effect.timeout("30 seconds")
  |> Effect.retry({ schedule: backoff, times: 4, while: flaky })
```

The fetch uses `Effect.tryPromise` so that interruption passes an `AbortSignal` to `fetch`. When a timeout fires or a caller is interrupted, the runtime aborts the HTTP request. Plain `await` is used for promises where abort signals do not apply.

The `flaky` predicate matches specific error variants by name, with `_` matching unhandled cases. The parse match includes a `_` wildcard because record patterns are structurally open (see [pattern matching](/reference/pattern-matching)).

The timeout error is tracked in the error channel type, requiring callers to handle potential timeouts.

Because the timeout, schedule, and predicate are values, they can be extracted into reusable helper functions.

## 3. Concurrency that cancels

When racing two requests with `Promise.race`, the promise settles as soon as the first finishes, but the losing request continues running unless manually cancelled.

Effect interrupts the losing fiber automatically.

```hem title="Hem"
let best = (prompt: string) => effect {
  Effect.race(askMistral(prompt), askGemini(prompt))!
}
```

Interruption propagates through retries, timeouts, and down to the abort signal of the underlying fetch. Similarly, if one member of an `and` group fails, the runtime interrupts the remaining operations, unlike `Promise.all`. Bounded concurrency is configured via options rather than external semaphore utilities.

```hem title="Hem"
let dashboard = (id: string) => effect {
  let user = fetchUser(id)! and orgs = fetchOrgs(id)!      // fail one, interrupt the other
  let scans = Effect.forEach(domains, scanJob, { concurrency: 10 })!
  {user, orgs, scans}
}
```

Hem supports the same `and` syntax for standard JavaScript promises over `Promise.all`. However, cancelling plain promises still requires manual signal coordination, whereas Effect handles interruption automatically across the group.

## 4. Resources that always release

Managing multiple resources in plain TypeScript requires nested `try`/`finally` blocks and manual ordering:

```ts title="Plain TypeScript"
const conn = await pool.connect();
try {
  const lock = await locks.acquire(domain);
  try {
return await runScan(conn, domain);
  } finally {
await locks.release(lock);
  }
} finally {
  conn.release();
}
```

If wrapped in a `Promise.race` with a timeout, the race resolves when the timeout triggers, but `runScan` continues running. Its `finally` blocks do not run until the underlying operation finishes. While `AbortController` can cancel individual APIs, plain promises do not tie cancellation to resource lifecycles automatically.

Effect manages resource lifetimes using scopes: acquisition is uninterruptible, release actions are registered atomically, and interruption triggers finalizers.

In Hem, the enclosing `effect` block serves as the scope:

```hem title="Hem"
let scanJob = (domain: string) =>
  effect {
use conn = Pool.acquire
use _    = Locks.hold(domain)
runScan(conn, domain)!
  } |> Effect.timeout("30 seconds")
```

When the timeout fires, the fiber is interrupted, the lock releases, the connection closes, and the timeout error propagates. Resource cleanup always runs before error recovery.

Creating resources for callers to manage uses `resource { }` blocks, which integrate with `Layer.effect`. Binding a scoped resource with `let` instead of `use` is a compile error:

```
HEM4105 · this effect acquires a resource — bind it with `use`
  scan.hem:12   let conn = Pool.acquire!
```

This prevents unmanaged resource leaks at compile time.

## 5. Dependencies you can swap

In plain TypeScript, managing dependencies usually requires passing them explicitly through signatures or using module-level mocks during testing.

Effect tracks required dependencies in the third type parameter and represents implementations as values (`Layer`). Hem provides concise syntax for declaring and accessing services.

```hem title="Hem"
service LLM {
  ask: (prompt: string) => Effect<Answer, LlmError>
}

// production wiring; the function from the second argument is the implementation
let LLMLive = Layer.succeed(LLM, { ask: askModel })

// a test provides a different world, with no module mocking anywhere
let LLMTest = Layer.succeed(LLM, { ask: (_) => Effect.succeed(cannedAnswer) })

it("classifies jurisdiction from the answer", () =>
  scanDomain("example.eu")
  |> Effect.provide(LLMTest)
  |> Effect.runPromise)
```

This also enables deterministic testing. Because the system clock is a service in Effect, retry tests run against a virtual test clock without waiting for real-world delays.

When required services are not provided, Hem reports the missing dependencies and the source lines where they were introduced:

```
HEM4102 · unprovided requirements
  routes/scan.hem:24  |> Effect.runPromise
  This effect still needs 2 services when run:
LLM   — enters at llm.ask(prompt)!      (line 11)
Pool  — enters at Pool.acquire          (line 9)
  Provide them first:  |> Effect.provide(AppLayer)
```

## The repeated application code, itemized

Everything above is available in standard Effect. Hem provides concise syntax for frequent application patterns and adds source-level compiler diagnostics.

| Raw Effect | Hem |
| --- | --- |
| `Effect.gen(function* () { … yield* e … })` | `effect { … e! … }` |
| `class X extends Data.TaggedError("X")<{…}> {}` | `error X({ … })` |
| `class S extends Context.Service<S, {…}>()("app/S") {}` | `service S { … }` |
| `yield* Effect.fail(new X({ id }))` | `raise X({ id })` |
| `Effect.tryPromise({ try: () => p, catch: f })` | `await p`, with `catch` arms to recover |
| `Effect.catchTags({ A: …, B: … })` | `catch { A(_) => … \| B(e) => … }` |
| Scope-wrapper placement puzzles | the block containing `use` |
| `Effect.all([a, b], { concurrency: "unbounded" })` and the sequential-default trap | `let x = a! and y = b!` |
| `pipe(a, f, g)` | `a \|> f \|> g` |
| `Type 'S' is not assignable to type 'never'` | HEM4102 with per-service blame lines |

## What it still costs, honestly

Hem simplifies Effect's syntax, but requires understanding its execution model.

Working with Effect in Hem still requires understanding lazy execution, the three effect channels (success, failure, requirements), and Effect's broader library modules. APIs such as `Schedule`, `Layer`, `Stream`, and `Config` remain standard library calls without special syntax.

Stack traces reflect the fiber runtime. Hem provides provenance diagnostics during compilation rather than runtime debugging.

Interfacing with promise-based libraries requires explicit lifting at boundary points.

For simple programs, such as a single HTTP request handler, plain `async`/`await` with `?T` and `match` in core Hem is often sufficient. The Effect integration is optional and configured per project.

## The compounding argument

Individual features like retries, concurrency limits, dependency injection, and resource disposal can be addressed with separate promise-based libraries.

Effect unifies these concerns into a single model where timeouts interrupt retries, abort HTTP requests, and release resources in order, with test clocks for simulation and compiler tracking for errors and requirements.

Hem provides direct language syntax for this model.

## Related

- [How to enable the Effect extension](/guides/enable-effect) explains how to configure the integration.
- [Effect boundaries](/reference/effect-boundaries) lists the compiler safety rules.
- [Part II of the tour](/tour/effect-a-description-not-an-action) explains each construct and its emitted TypeScript.

Source: https://hem.run/explanation/why-effect-why-hem/index.mdx
