---
title: "Errors and failures"
description: "Core errors, raise, postfix catch, and try; which failures types track; the same vocabulary under Effect."
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.

# Errors and failures

Hem provides error handling in the zero-dependency core. `error` declares a nominal
error class, `raise` throws one, and postfix `catch` handles selected failures. The Effect
extension uses the same syntax but changes how failures propagate and what the types record: core
code throws through the JavaScript stack, while an `effect {}` block fails a typed error channel.

## What the types track

In core, error _values_ are typed. An `error` declaration is a class, so a raised instance has a
precise type and a catch arm matches it by tag. Raising is not typed. A core `raise` is a
JavaScript exception: the enclosing function's type does not mention it, callers' types do not
inherit it, and no caller is required to handle it.

```hem
error Boom({ why: string })

export let risky = (n: number) => {
  if n > 3 { raise Boom({ why: "big" }) }
  n * 2
}

export let caller = (n: number): number => risky(n)
```

```ts emitted
class Boom extends Error {
  readonly _tag = "Boom";
  readonly why: string;

  constructor(fields: { readonly why: string }) {
super("Boom");
this.name = "Boom";
this.why = fields.why;
  }
}
export const risky = (n: number) => {
  if (n > 3) {
throw new Boom({ why: "big" });
  }
  return n * 2;
};
export const caller = (n: number): number => risky(n);
```

Both `risky` and `caller` have the type `(n: number) => number`. Nothing in either signature
records that `Boom` can escape, and `caller` compiles without handling it.

When a failure must be visible in a signature, choose a mechanism that records it:

| Mechanism | Where the failure appears | What a caller must do |
| --- | --- | --- |
| Core `raise` | Nowhere in the function type | Nothing; an unhandled error propagates at run time |
| `Result<T, E>` from `@hem-lang/std/result` | The return type's `E` | Inspect the must-use result, or propagate it with `let Ok(x) = ...` |
| `raise` in an `effect {}` block | The Effect's error channel `E` | Handle it with `catch`, or leave it in the caller's `E` |

Returning a canonical `Result` keeps core code free of Effect while making the failure part of
the contract:

```hem
import { Result } from "@hem-lang/std/result"

error Boom({ why: string })

export let risky = (n: number): Result<number, Boom> =>
  if n > 3 { Result.err(new Boom({ why: "big" })) } else { Result.ok(n * 2) }

export let caller = (n: number): Result<number, Boom> => {
  let Ok(doubled) = risky(n)
  Result.ok(doubled + 1)
}
```

`let Ok(doubled)` without `else` returns the original `Err` from `caller`, so the error union
stays in `caller`'s return type. See [standard Option and Result](/reference/standard-carriers)
for carrier binding and the Result API, and [Effect boundaries](/reference/effect-boundaries) for
the error channel.

## Error declarations

`error Name` declares a tagged error class. A record payload adds its fields to the class;
the class also has a readonly `_tag` and extends `Error`. Declared `cause` and `message` fields
are forwarded to the `Error` constructor. `export error` exports the class.

```hem
error MissingUser({ id: string })

let findUser = (id: string): string => {
  if id == "" { raise MissingUser({ id }) }
  id
}
```

In core output, raising a local error with a record payload automatically inserts `new`. Imported
error classes cross TypeScript boundaries, so construct them explicitly with `new` in both
core and Effect code. Under the Effect extension, the same declaration emits a
`Data.TaggedError`; both forms extend `Error` and carry `_tag`, so code compiled in either
mode can match the other. See [variants, brands, and errors](/reference/variants-and-brands) for
the declaration shape.

## `raise`

`raise e` terminates execution at the point where it appears. In core it compiles to a throw,
which no type records. In an Effect block it compiles to `yield* Effect.fail(e)`, adding `e` to
the block's checked error channel. Because it
diverges, `raise` is valid in a guard branch and does not need to produce the surrounding value's
type.

In both worlds Hem constructs locally declared errors for you: `raise MissingUser({ id })` emits
`new MissingUser({ id })`, and a payload-free error is raised by name, so `raise DecodeFailed`
emits `new DecodeFailed()`. Imported error classes take an explicit `raise new Name(…)`. Raising a
bare constructor instead of an instance (a local error with fields named without them, or an
imported class without `new`) triggers HEM2034.

`throw` remains standard JavaScript for defects and rethrows. In an Effect
block, a `throw` is not a typed failure and is not handled by an `Effect.catch` arm.

## Postfix `catch`

`catch` has the lowest operator precedence, attaching to the complete expression on its left, including an
`await` expression. Its arms are the same in core and Effect:

| Arm | What it handles |
| --- | --- |
| `Name(_) => value` | A named error, discarding its instance |
| `Name(error) => value` | A named error, binding its instance |
| `Name({ field }) => value` | A named error, destructuring its fields |
| `Name({ field }) as error => value` | Both the fields and the error instance |
| `error => value` | Any failure; `error` is `unknown` in core |

Named arms use `instanceof` in core. A binding arm is the catch-all arm. Handle only the failures you
intend to recover: an unmatched failure propagates. Core rethrows it; Effect leaves it in the
remaining error channel. HEM2011 reports a tag that is not an error class in core or is absent
from the Effect error channel. If TypeScript exposes `any` for the caught expression, Hem may
be unable to check a declared error arm against either channel. Give the expression a known
type, such as `Effect<A, Offline>`, so Hem can check what the catch handles.
In core, the compiler does not check a handler against what its subject can throw, because no type records that; under Effect, arms are checked against the inferred error channel.

### Synchronous code

The handler is an expression, so its result becomes the result of the whole postfix expression.
This uses standard stack-based exception handling in core.

```hem
error ParseFailed({ input: string })

let parse = (input: string): string => {
  if input == "" { raise ParseFailed({ input }) }
  input
}

let parseOrDefault = (input: string): string =>
  parse(input) catch {
  | ParseFailed(_) => "default"
  }
```

The `ParseFailed` arm handles only that class. If `parse` throws another error, it leaves this
handler and propagates up the caller's stack.

### Async code

Await before handling a promise rejection. A handler after a bare promise observes promise
construction, not its eventual rejection; Hem reports a hint for that form.

```hem
error GatewayFailed({ url: string })

let request = async (url: string): Promise<string> => url

let load = async (url: string): Promise<string> =>
  await request(url) catch {
  | _ => raise GatewayFailed({ url })
  }
```

In a standard async arrow, this compiles to core try/catch around the awaited operation. In an
`effect {}` block, the same `await ... catch` handles the rejection at the Effect boundary; an
uncaught rejection becomes the configured library's `Cause.UnknownError`.

## `try` for protected regions

Postfix `catch` is the compact form. `try { ... } catch pattern { ... }` is the expression
form for a protected region containing several statements. The optional binding is a pattern; a
plain binding has type `unknown` and must be narrowed before use. Both the protected block and the
handler block can return a value. Unmatched failures propagate.

```hem
error DecodeFailed

let decode = (input: string): string =>
  try {
if input == "" { raise DecodeFailed }
input
  } catch problem {
if problem instanceof DecodeFailed { "default" } else { throw problem }
  }
```

`try` produces a value even when it contains multiple statements. In statement position, it
runs for its effects. The value-position lowering rules, including where a nested protected
region can be hoisted, are in [expressions and control flow](/reference/control-flow).

## Effect continuity

Effect is opt-in through `effectImportSource`; without it, these constructs introduce no runtime
imports. With it, the syntax remains unchanged:

- `raise` puts a declared error on the typed `E` channel.
- `catch` handles the error channel of an Effect-valued expression.
- `await` directly in an `effect` block lifts a promise through `Effect.tryPromise`; `catch` can
  translate its rejection by raising a domain error. Inside an `async` arrow nested in the block,
  `await` is native JavaScript `await`.
- `throw` remains a defect.
- These meanings stop at a function boundary. In an arrow nested inside an `effect` block,
  `raise` is the core `throw`, so an uncaught one reaches the block as a defect rather than a
  typed failure; a nested `effect` block is typed again.

An Effect catch handler's pure result is lifted into a successful Effect, an Effect result passes
through, and an `effect {}` handler body becomes a nested Effect block. A named arm narrows the
error channel; `_` handles the typed failure channel, not defects. Resource cleanup runs before a
handler executes, so a `catch` arm never observes a live scoped resource. The [Effect boundaries
reference](/reference/effect-boundaries) documents the channel, scope, and execution-edge rules.

## Related rules

- [Absence (`?T`)](/reference/absence) covers missing values, which are not thrown failures.
- [Standard Option and Result](/reference/standard-carriers) covers explicit eager carriers and
  their separate Effect adapter.
- [Pattern matching](/reference/pattern-matching) covers the patterns shared by `match`, `let`, and
  `catch`.

## Normative source

Specification 0.2, §9 (core errors and exceptions), §7 (patterns and control flow), and §14 (Effect
transport).

Source: https://hem.run/reference/typed-failures/index.mdx
