Skip to main content

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.
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().
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().
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.

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:
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}`),
  });
}