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

# Do Notation

> Write async pipelines imperatively with generators.

## What is do-notation?

Do-notation is a pattern borrowed from functional languages that lets you write monadic pipelines as straightforward imperative code. Instead of nesting `.andThen()` callbacks, you write a generator function where each `yield*` statement either unwraps the `Ok` value and continues, or short-circuits the entire function with the `Err`.

The result is code that reads top-to-bottom like ordinary async/await, while still being fully type-safe and never silently swallowing errors.

***

## `chas.go()` for Result and ResultAsync

`chas.go()` runs an `async function*` generator and returns a `ResultAsync`. Inside the generator, you can `yield*` both synchronous `Result` values and `ResultAsync` values; they're both handled the same way.

**How it works:**

* `yield* someResult` — if `someResult` is `Ok`, the expression evaluates to the inner value; if it's `Err`, the generator exits immediately and `chas.go()` returns that `Err`.
* The `return` value at the end becomes the `Ok` value of the final `ResultAsync`.
* The `Err` type of the returned `ResultAsync` is the union of all error types produced by every `yield*` call.

```typescript theme={null}
import { chas } from 'ts-chas';

declare function fetchUser(id: string): chas.ResultAsync<User, NetworkError>;
declare function parseConfig(prefs: string): chas.Result<Config, ParseError>;
declare function saveProfile(user: User, config: Config): chas.ResultAsync<void, DbError>;

const result = await chas.go(async function* () {
  const user = yield* fetchUser('user-123');     // ResultAsync<User, NetworkError>
  const config = yield* parseConfig(user.prefs); // Result<Config, ParseError>
  yield* saveProfile(user, config);              // ResultAsync<void, DbError>
  return { user, config };
});
// Result<{ user: User; config: Config }, NetworkError | ParseError | DbError>
```

TypeScript collects every error type from every `yield*` into the final `Err` union, so the compiler tells you exactly which failures you need to handle.

***

## `Task.go()` for Task pipelines

When your pipeline is built entirely from `Task` instances (especially when you want lazy execution, retries, or dependency injection) use `Task.go()` instead. It has the same `yield*` semantics but returns a `Task` you can configure further before calling `.execute()`.

```typescript theme={null}
import { Task } from 'ts-chas/task';

declare function fetchUserTask(id: string): Task<User, NetworkError>;
declare function parseConfigTask(prefs: string): Task<Config, ParseError>;
declare function saveProfileTask(user: User, config: Config): Task<void, DbError>;

const profileTask = Task.go(function* () {
  const user = yield* fetchUserTask('user-123');
  const config = yield* parseConfigTask(user.prefs);
  yield* saveProfileTask(user, config);
  return { user, config };
});
// Task<{ user: User; config: Config }, NetworkError | ParseError | DbError>

// Configure resilience before running
const result = await profileTask
  .retry(3, { delay: 500, factor: 2 })
  .execute();
```

Because `Task` is lazy, nothing runs until you call `.execute()`. You can attach `.retry()`, `.timeout()`, `.circuitBreaker()`, and other operators to the composed task after building it with `Task.go()`.

***

## Side-by-side comparison

The example below fetches a user, parses their config, and saves their profile. Both versions are equivalent: one uses chained `.andThen()` calls, the other uses `chas.go()`.

<Tabs>
  <Tab title=".andThen() chains">
    ```typescript theme={null}
    import { chas } from 'ts-chas';

    const result = await fetchUser('user-123')
      .andThen((user) =>
        parseConfig(user.prefs)
          .asyncAndThen((config) =>
            saveProfile(user, config).map(() => ({ user, config }))
          )
      );
    ```

    This works, but the nesting grows with each step. You also need `asyncAndThen` the moment any step returns a `ResultAsync`, and you must manually close the variable scope to reference earlier values in later steps.
  </Tab>

  <Tab title="do-notation with chas.go()">
    ```typescript theme={null}
    import { chas } from 'ts-chas';

    const result = await chas.go(async function* () {
      const user = yield* fetchUser('user-123');
      const config = yield* parseConfig(user.prefs);
      yield* saveProfile(user, config);
      return { user, config };
    });
    ```

    Every step is flat. Earlier values remain in scope naturally. You don't need to distinguish between `andThen` and `asyncAndThen` — `yield*` handles both `Result` and `ResultAsync` transparently.
  </Tab>
</Tabs>

***

## Error handling after `chas.go()`

The return value of `chas.go()` is a `ResultAsync`, so you handle errors the same way you would with any other result:

```typescript theme={null}
import { chas } from 'ts-chas';

const AppError = chas.defineErrs({
  NetworkError: (url: string) => ({ url }),
  ParseError: (reason: string) => ({ reason }),
});

const result = await chas.go(async function* () {
  const raw = yield* fetchRaw('https://api.example.com/data');
  const parsed = yield* parsePayload(raw);
  return parsed;
});

if (result.isErr()) {
  chas.matchErrPartial(result.error, {
    NetworkError: (e) => console.error(`Failed to fetch ${e.url}`),
    _: (e) => console.error(`Unexpected error: ${e.message}`),
  });
}
```
