# Effects

URL: /docs/tour/effects
Markdown: /docs/tour/effects.md

> How Sakura Lang records every side effect and every expected failure in the function signature, and how effect rows compose.

> **Pre-alpha**
>
> The source-built compiler runs the core language and much of M3 on JavaScript: contracts, typed effects and failures, std handlers, permissions, LSP and MCP. M3 is still in progress; no release or stable compatibility contract exists. See the [roadmap](/docs/roadmap) for implemented features and remaining gates.

An effect is anything a function does besides compute its return value: read the clock, print, query a database, call the network, fail. In Sakura Lang, every effect a function may perform is listed in its signature, after `!`.

```sakura
fn signup(email: Email, pw: Password) -> User ! {Db, Clock, Random, Fail<SignupError>}
```

The braces hold the function's **effect row**. `signup` returns a `User` and may also use `Db` (a user-defined effect, declared below), the clock, and randomness, and may fail with a `SignupError`. The row is an upper bound: the body may do less, never more.

## Pure functions [#pure-functions]

A signature with no `!` is pure: `! {}`. This is the one meaning of omission, for top-level signatures and for written function types alike.

```sakura
fn total(prices: List<Int>) -> Int {
  List.fold(prices, 0, fn(acc, p) { acc + p })
}
```

`! {}` means "no declared operations and no expected failures". It does not mean "cannot crash": overflow and contract violations are [defects](#defects-are-not-effects), which are not effects.

## Performing effects [#performing-effects]

An operation is performed with `Effect.op(args)`. Calling a function performs that function's row. Creating a closure performs nothing; calling it performs its row.

```sakura
fn greet(name: String) -> Unit ! {Console, Clock} {
  let now = Clock.now()
  Console.print("hello, {name}; it is {Instant.format_iso(now)}")
}

pub fn main() -> Unit ! {Console, Clock} { greet("Sakura Lang") }
```

`main` calls `greet`, so `main`'s row must include `Console` and `Clock`.

## Declaring effects [#declaring-effects]

An `effect` declares operations as function signatures, with no bodies:

```sakura
error DbUnavailable(reason: String)

effect Db {
  fn exists(table: Table, key: String) -> Bool
  fn insert(table: Table, row: Json) -> Unit ! {Fail<DbUnavailable>}
}
```

An operation's row may contain `Fail<…>`, plus row variables of the operation's own type parameters that also appear in a function-typed parameter, so an operation that runs a callback performs what the callback performs (`fn scope<T, e>(body: fn(Scope) -> T ! e) -> T ! e`). Any other label is `V3010`. The declaration says what operations exist, not what they do. That is the job of a [handler](/docs/tour/handlers), chosen by the caller.

## Rows are sets [#rows-are-sets]

A row is a set of unique labels plus an optional open tail: `{Db, Clock | e}`. A label is an effect plus its type arguments, such as `Fail<SignupError>`. Order does not matter; the formatter sorts labels alphabetically with `Fail<…>` last. Sequencing two expressions takes the union of their rows. See design §5.2 for the unification rules.

## Effect polymorphism [#effect-polymorphism]

A higher-order function should not fix the effects of the function it receives. A lowercase type parameter is an **effect-row variable**:

```sakura
fn map<A, B, e>(xs: List<A>, f: fn(A) -> B ! e) -> List<B> ! e
```

If `f` is pure, the call is pure. If `f` logs and may fail, the call logs and may fail. An **open row** names some labels and leaves the rest to a variable:

```sakura
fn traced<T, e>(name: String, body: fn() -> T ! e) -> T ! {Log | e} {
  Log.event(Level.Info, "start {name}", [])
  let value = body()
  Log.event(Level.Info, "end {name}", [])
  value
}
```

A top-level function must declare its row variables (`<e>`). A function value with a closed row is accepted where a larger row is expected: `fn() -> Int ! {Clock}` can be passed where `fn() -> Int ! e` is expected, and `e` becomes `{Clock}`. This widening is one of the three places Sakura Lang has subtyping, with error unions and refinement-to-base.

## Failure: `Fail<E>` and error unions [#failure-faile-and-error-unions]

There are no exceptions. Expected failure is `Fail<E>`, the one compiler-known effect. `E` is an **error union**: a set of nominal `error` tags.

```sakura
error EmailTaken(email: Email)
error Banned
type SignupError = EmailTaken | Banned           // union alias: tags only
```

`fail EmailTaken { email }` has type `Never` and adds `Fail<EmailTaken>` to the current row. `Fail` occurs at most once in a row. When two failing expressions are sequenced, their unions join: `Fail<A> ∪ Fail<B> = Fail<A | B>`.

Propagation is automatic. Calling a function with `Fail<A>` adds `Fail<A>` to the caller's row, with no operator needed. A union `U` is accepted where `V` is expected if `U ⊆ V`. When two different unions meet with no expected type, the compiler asks for an annotation (`V2015`).

Recovery uses `try`/`catch`. Arms are patterns over the body's tags. Tags matched by an unguarded arm are removed from the row; unmatched tags propagate:

```sakura
error NotFound(id: String)
error Forbidden
error Unavailable(reason: String)
type UserError = NotFound | Forbidden
type User = { id: String, owner: String, name: String }

effect Users {
  fn get(id: String) -> Option<User> ! {Fail<Unavailable>}
}

pub fn view_user(caller: String, id: String) -> User ! {Users, Log, Fail<UserError>} {
  let user = try {
    Users.get(id)
  } catch {
    Unavailable { reason } => {
      Log.event(Level.Warn, "users backend unavailable", [("reason", Json.String(reason))])
      None
    }
  }
  match user {
    None => fail NotFound { id },
    Some(u) if u.owner != caller => fail Forbidden,
    Some(u) => u,
  }
}
```

The internal `Unavailable` tag is translated at the boundary, so the public union stays small and stable. Other ways to move between failure and data:

| Form | Result |
| --- | --- |
| `try { body }` with no `catch` | `Result<T, E>` with the body's full union; `Fail` leaves the row |
| `r?` on `r: Result<T, E>` | `T`, and adds `Fail<E>` to the row |
| `Option.or_fail(o, err)` | the value, or `fail err` on `None` |

`?` applies only to a `Result`. A call to a function with `Fail<E>` in its row needs no operator; its failure propagates on its own.

A `try` body cannot assign to a `var` declared outside it (`V2041`), because the body may stop at any failing call and leave the assignment half done. Its value and its error are the only things that leave it: assign after the `try`, or declare the `var` inside.

There is no implicit conversion between error types. A catch-all `_ => …` arm is allowed but discouraged on open unions (`W4010`).

## Defects are not effects [#defects-are-not-effects]

A **defect** is a bug, not an expected failure. It terminates the current task after running resource finalizers. `try` cannot catch it. `Task.scope` and the test runner observe it as `Outcome.Defect(info)`, and `main` ending in a defect exits with status 70 and writes a `defect` record to stderr. By contrast, a typed failure that escapes `main` exits with status 1 and prints `error: main failed with <Show of the error>`.

| Code | Defect |
| --- | --- |
| `D001` | `Int` overflow |
| `D002` | division by zero |
| `D003` | `Int` beyond ±(2^53−1) on the JavaScript backend |
| `D010` | failed checked introduction, such as `Email.from(s)` |
| `D020` / `D021` | `requires` violated (caller blamed) / `ensures` violated (callee blamed) |
| `D030` | `Option.expect` on `None` |

There is no user-callable way to raise a defect. Express a programming error with `requires` or `Option.expect`.

## Standard effects [#standard-effects]

From design §10.2. Each effect lives in its own std module (`std/console`, `std/http`, …). The first seven are in the prelude; the rest must be imported by name, for example `import std/http.{Http}`.

| Effect | Operations | Purpose |
| --- | --- | --- |
| `Console` | `print`, `read_line` | Terminal IO |
| `Clock` | `now`, `monotonic`, `sleep` | Time |
| `Random` | `int`, `float`, `bytes`, `uuid` | Randomness |
| `Env` | `get`, `secret` | Environment variables: `get` returns `Option<String>`, `secret` returns `Option<Secret<String>>` |
| `Log` | `event` | Structured logging |
| `Task` | `scope`, `spawn`, `join`, `all`, `race`, `yield_now` | [Structured concurrency](/docs/tour/concurrency) |
| `Test` | `record_failure`, `output_lines`, `snapshot`, … | Assertions inside `test` and `property` bodies |
| `Fs` | `read`, `read_text`, `write`, `list`, `exists`, `with_file` | Files |
| `Http` | `send` | HTTP client |
| `Serve` | `listen` | HTTP server |
| `Sql` | `query`, `execute`, `transaction` | SQL database |
| `Cli` | `args`, `exit` | Command-line arguments and exit status |
| `Budget` | `reserve`, `settle` | Token, cost, and call limits |
| `Llm` | `generate`, `generate_text`, `stream` | Language model calls |
| `Fail<E>` | `fail` | Expected failure |

There is no `Async` effect. Every Sakura Lang function may suspend; whether a call suspends is decided by its handler (design §5.6). See [Concurrency](/docs/tour/concurrency).

## Inference [#inference]

Inside a body, the checker infers a row for every expression together with its type (design §5.7):

| Form | Row |
| --- | --- |
| literal, variable, lambda, constructor, `handler {…}` | `{}` |
| `f(a1..an)` | rows of `f` and the arguments, plus `f`'s latent row |
| `Eff.op(args)` | rows of the arguments, plus `{Eff}` and the op's declared `Fail` |
| `fail e` | row of `e`, plus `Fail<tag(e)>` |
| `if` / `match` | condition or scrutinee, plus the union of all branches |
| `with h1..hn { b }` | row of `b` minus the handled effects, plus each handler's row |
| `try { b } catch { arms }` | row of `b` with matched tags removed, plus the arms' rows |
| `try { b }` | row of `b` minus `Fail` |
| `r?` | row of `r`, plus `Fail<E>` |

Branch rows join conservatively: an effect in an unreachable branch still counts. Evaluation is strict and left to right.

Top-level rows are never inferred. Every top-level `fn` states its row, so a change in a callee's effects cannot silently change a caller's contract.

## Undeclared effects [#undeclared-effects]

If a body performs an effect its signature does not allow, `sakura check` reports `V3001`:

```sakura title="src/users/repo.sakura"
type User = { id: String, name: String, seen: Instant }

effect Db {
  fn get_user(id: String) -> Option<User>
}

pub fn load_user(id: String) -> Option<User> ! {Clock} {
  let now = Clock.now()
  let found = Db.get_user(id)
  Option.map(found, fn(u) { User { ..u, seen: now } })
}
```

`sakura check --json` emits one JSON Lines record per diagnostic, in the design §12.2 shape. Pretty-printed, with hashes shortened:

```json
{
  "schema_version": "1.0",
  "kind": "diagnostic",
  "id": "d:7f3a…",
  "code": "V3001",
  "severity": "error",
  "phase": "effect",
  "message": "function `load_user` performs `Db`, which its signature does not allow",
  "snapshot": "sha256:…",
  "symbol_id": "pkg::users/repo::load_user",
  "spans": [
    {
      "uri": "file:///…/src/users/repo.sakura",
      "content_hash": "sha256:…",
      "start_byte": 159,
      "end_byte": 168,
      "role": "primary",
      "label": "allowed here: `{Clock}`"
    },
    {
      "uri": "file:///…/src/users/repo.sakura",
      "content_hash": "sha256:…",
      "start_byte": 209,
      "end_byte": 224,
      "role": "cause",
      "label": "`Db.get_user` introduces `Db` here"
    }
  ],
  "related_ids": [],
  "expected": {
    "render": "{Clock}",
    "row": { "labels": [{ "effect": "std::Clock", "args": [] }], "tail": null }
  },
  "actual": {
    "render": "{Clock, Db}",
    "row": {
      "labels": [
        { "effect": "std::Clock", "args": [] },
        { "effect": "pkg::users/repo::Db", "args": [] }
      ],
      "tail": null
    }
  },
  "obligation": null,
  "counterexample": null,
  "explanation_uri": "sakura://explain/V3001",
  "fixes": [
    {
      "id": "f:1",
      "title": "Add `Db` to the signature of `load_user`",
      "applicability": "suggested",
      "impact": "interface_change",
      "edits": [
        {
          "span": {
            "uri": "file:///…/src/users/repo.sakura",
            "content_hash": "sha256:…",
            "start_byte": 159,
            "end_byte": 168,
            "role": "primary",
            "label": "signature row"
          },
          "replacement": "! {Clock, Db}"
        }
      ]
    },
    {
      "id": "f:2",
      "title": "Handle `Db` locally by installing a handler around the call",
      "applicability": "incomplete",
      "impact": "local",
      "edits": [
        {
          "span": {
            "uri": "file:///…/src/users/repo.sakura",
            "content_hash": "sha256:…",
            "start_byte": 209,
            "end_byte": 224,
            "role": "cause",
            "label": "call to `Db.get_user`"
          },
          "replacement": "with ?db_handler { Db.get_user(id) }"
        }
      ]
    }
  ]
}
```

Byte offsets are zero-based and half-open. There are two honest ways out. If `load_user` really should touch the database, the signature changes; that fix is an `interface_change`, so it is never `machine_applicable` and a reviewer sees it. If it should not, the call is handled locally; that fix leaves a typed hole for the handler, so it is `incomplete`. See [Diagnostics](/docs/agents/diagnostics).

## Effects are requirements [#effects-are-requirements]

`Fs` in a row says "may perform filesystem operations". It does not say which files. The installed handler decides that, and handlers are built from capability values the program cannot forge. See [Capabilities and security](/docs/tour/capabilities).

## What the compiler checks [#what-the-compiler-checks]

- The inferred row of a body is a subset of the declared row. Each extra label is `V3001`, with the call that introduced it.
- A label that a closed row does not permit is `V3002`.
- Every `Fail` tag is either caught or declared. Union mismatches without an expected type are `V2015`.
- Effect operations declare only `Fail` and their callbacks' row variables in their rows (`V3010`).
- Top-level rows and row variables are declared, never inferred.

## Next [#next]

[Handlers](/docs/tour/handlers): how a caller decides what an effect actually does.
