# Concurrency

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

> Structured concurrency with the Task effect, cooperative cancellation, timeouts, retries, budgets, and deterministic scheduling in Sakura Lang.

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

Sakura Lang has no `async`/`await` keywords and no `Async` effect. Every Sakura Lang function may suspend, and nothing in application code can observe whether a call did. Suspension is a property of **handlers**: `Clock.sleep` suspends under the host clock and returns immediately under `Clock.fixed` (design §5.6).

Running work concurrently is a different matter. That uses the `Task` effect from `std/task`, which is in the prelude.

## The `Task` effect [#the-task-effect]

```sakura
// std/task
pub type Scope          // external, scoped
pub type Fiber<T>       // external, scoped, affine
pub effect Task {
  fn scope<T, e>(body: fn(Scope) -> T ! e) -> T ! e
  fn spawn<T, e>(s: Scope, body: fn() -> T ! e) -> Fiber<T> ! e
  fn join<T>(f: Fiber<T>) -> T
  fn all<T, e>(bodies: List<fn() -> T ! e>) -> List<T> ! e
  fn race<T, e>(bodies: List<fn() -> T ! e>) -> T ! e
  fn yield_now() -> Unit
}
pub fn host() -> Handler<Task>
pub fn deterministic(seed: Int) -> Handler<Task>
pub fn par_map<A, B, e>(xs: List<A>, f: fn(A) -> B ! e) -> List<B> ! {Task | e}
```

`Scope` and `Fiber<T>` are external types: the host runtime owns their representation and user code cannot construct them. A function that uses these operations has `Task` in its row. The child bodies' rows flow into the caller's row through `e`.

## Scopes [#scopes]

`Task.scope` opens a scope. `Task.spawn` starts a child in it and returns a `Fiber<T>`. `Task.join` waits for the child's result.

```sakura
import std/http.{Http, HttpError, RateLimited, Response}
import std/url.{Url}

fn load_both(a: Url, b: Url) -> (Response, Response) ! {Http, Task, Fail<HttpError | RateLimited>} {
  Task.scope(fn(s) {
    let fa = Task.spawn(s, fn() { Http.get(a) })
    let fb = Task.spawn(s, fn() { Http.get(b) })
    (Task.join(fa), Task.join(fb))
  })
}
```

Children never outlive their `Scope`. This gives three rules:

- **Failure propagates.** If a child fails or defects, the scope cancels its siblings, joins them, and propagates the first failure. Other failures are attached as `suppressed` in the outcome.
- **Defects are observed, not caught.** `Task.scope` observes a child's defect as `Outcome.Defect(info)`. `try` cannot catch it (design §5.4).
- **Resources are released.** A `Fiber<T>` is a resource: affine and bound to its scope (design §8.4).

`Task.all` runs a list of thunks and returns their results in order. `Task.race` returns the result of the first to finish. `Task.par_map`, a function of the `std/task` module, maps in parallel:

```sakura
fn fetch_all(urls: List<Url>) -> List<Response> ! {Http, Task, Fail<HttpError | RateLimited>} {
  Task.par_map(urls, fn(u) { Http.get(u) })
}
```

## Cancellation is cooperative [#cancellation-is-cooperative]

A cancelled task stops at its next effect operation or at `Task.yield_now()`. Pure loops are not interruptible. A long pure computation that should respond to cancellation calls `Task.yield_now()` now and then:

```sakura
fn total_size(chunks: List<List<Int>>) -> Int ! {Task} {
  var total = 0
  for chunk in chunks {
    Task.yield_now()
    total = total + List.sum(chunk)
  }
  total
}
```

## Handlers in child tasks [#handlers-in-child-tasks]

Children inherit the handler environment at spawn time. A handler's `var` state is accessed serially, so two children performing the same effect go through the same instance one operation at a time (design §5.5).

## No data races [#no-data-races]

Values have value semantics and closures capture by value (design §8.1). There is no shared mutable memory, so data races cannot occur. Tasks communicate through their results (`join`, `all`, `race`) and through handler state. There is no channel type in v0; bounded channels may be added as `Task` operations when a program needs streaming between fibers (design §16.13).

Logical races with the outside world, such as two tasks reading and then writing the same database row, are still possible. The type system does not prove their absence.

## Detached work [#detached-work]

There is no fire-and-forget spawn and no detached work in v0: every fiber belongs to a scope. A supervisor for long-running work is deferred (design §16.14). A capability, or a handler that captured one, may not flow into a `Task.spawn` closure whose scope outlives the `with` that produced it (`V6002`).

## Timeouts, retries, and budgets [#timeouts-retries-and-budgets]

All three are ordinary std functions over thunks (design §5.9):

```sakura
// std/retry
type Policy<E> = { retry_on: fn(E) -> Bool, max_attempts: Int, backoff: Duration, jitter: Bool, deadline: Option<Duration> }
fn policy<E>(retry_on: fn(E) -> Bool, max_attempts: Int) -> Policy<E> requires max_attempts >= 1
fn retry<T, E, e>(p: Policy<E>, action: fn() -> T ! {Fail<E> | e}) -> T ! {Clock, Random, Fail<E> | e}
// std/task
error Timeout(limit: Duration)
fn timeout<T, e>(limit: Duration, action: fn() -> T ! e) -> T ! {Clock, Task, Fail<Timeout> | e}
// std/budget: a handler, not a wrapper function
type Limits = { tokens: Option<Int>, usd: Option<Float>, calls: Option<Int> }   // None = unlimited
fn limits(l: Limits) -> Handler<Budget>
```

- `retry` starts a **new invocation** per attempt. It never resumes a failed one. `retry_on` is a pure predicate over the action's error union `E`, which is why `Policy` takes the error type as a parameter. `retry.policy(retry_on, max_attempts)` builds a policy with default backoff; the record fields can also be set directly. The first retry waits `backoff`, each later wait doubles, `jitter` randomizes the wait, and `deadline` bounds the total time. When attempts run out, the last error propagates; no extra "exhausted" tag is added. The std HTTP client refuses to retry non-idempotent methods without an idempotency key.
- `timeout` cancels the action's task at its next effect operation, joins it, then fails with `Timeout { limit }`. Finalizers run. `std/task.Timeout` is the only timeout tag in std; `LlmError` reuses it.
- `Budget` is an effect with `reserve` and `settle`. `Llm` and `Http` handlers consume it when it is installed. Exceeding it fails with `BudgetExceeded`, whose `kind` is `Tokens`, `Usd`, `Calls`, `Bytes`, or `Time`. Child tasks inherit the budget, split atomically.

Together, from design §15.10:

```sakura
import std/http.{Http, HttpError, RateLimited, Response, Any}
import std/url.{Url}
import std/retry.{Policy, retry}
import std/task.{Timeout, timeout}

fn fetch_both(a: Url, b: Url) -> (String, String) ! {Http, Task, Clock, Random, Fail<HttpError | RateLimited | Timeout>} {
  let policy = Policy { retry_on: fn(e) { match e { RateLimited { .. } => true, _ => false } },
                        max_attempts: 3, backoff: Duration.millis(200), jitter: true, deadline: None }
  timeout(Duration.seconds(5), fn() {
    Task.scope(fn(s) {
      let fa = Task.spawn(s, fn() { retry(policy, fn() { Response.text(Http.get(a), 1_000_000).trust(@trust("display only")) }) })
      let fb = Task.spawn(s, fn() { retry(policy, fn() { Response.text(Http.get(b), 1_000_000).trust(@trust("display only")) }) })
      (Task.join(fa), Task.join(fb))
    })
  })
}
```

A budget is installed as a handler and configured with a `Limits` record. From `main` in design §15.7:

```sakura
let limits = Limits { tokens: Some(20_000), usd: Some(0.50), calls: None }
with Llm.anthropic(key, Model.of("claude-sonnet-5-5")), Budget.limits(limits) {
  let t = extract_ticket(Untrusted.of("Login page 500s since 9am, blocking all customers"))
  Console.print("{t.priority}: {t.title}")
}
```

## Determinism [#determinism]

Evaluation is strict and left to right, arguments before the call. `Map` and `Set` iterate in insertion order on every backend. `Clock`, `Random`, and `Task` are effects, so a test can replace all three (design §8.7):

```sakura
test "deterministic under virtual time" {
  with Http.scripted([(Any, Response.status(200, "x"))]), Clock.virtual(), Random.seeded(1), Task.deterministic(1) {
    expect_eq(fetch_both(Url.from("https://a"), Url.from("https://b")), ("x", "x"))
  }
}
```

Under `Task.deterministic(seed)` scheduling is reproducible. `Clock.virtual()` advances only when told to. This is how `with` doubles as Sakura Lang's deterministic-simulation mechanism. See [Testing](/docs/tour/testing).

## Runtime model [#runtime-model]

The first backend compiles to JavaScript with a CPS trampoline and is single-threaded. A live host handler, such as `Clock.sleep` or `Http.send`, suspends by returning a step the driver awaits (design §13.3). The native backend (M5) will run tasks on worker threads and promote values sent across threads to atomic reference counts.

## Not in v0 [#not-in-v0]

- An `Async` effect, `async`/`await`, or colored functions (design §5.6).
- Durable workflows. They are a later official package, not part of the language (design §1.1, §11.3).
- Actors. `spawn` and `actor` are reserved keywords (`Task.spawn` is legal because a reserved word is accepted after `.`).
- Channels, and detached or supervised tasks.
- Shared mutable state or locks in user code.

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

- Code that uses `Task` operations declares `Task` in its row (`V3001` otherwise).
- Child rows flow into the parent's row through the `e` variables of `Task`'s operations.
- A capability or capturing handler does not escape into a task that outlives its scope (`V6002`).
- A `Fiber<T>` does not outlive its scope.

## Next [#next]

That is the end of the tour. Continue with [For Agents](/docs/agents) to see how agents work with the toolchain, or the [Standard library](/docs/stdlib).
