Skip to main content
Result<T, E> represents either a success (Ok<T>) or a failure (Err<E>). Unlike try/catch, errors appear directly in the function signature, so TypeScript enforces that you handle them.
// The caller has no idea this can fail
function getUser(id: string): User { ... }

// The caller knows exactly what can go wrong
function getUser(id: string): Result<User, 'not_found' | 'db_error'> { ... }

Creating results

import { chas } from 'ts-chas';

const success = chas.ok(42);          // Ok<number>
const failure = chas.err('not found'); // Err<string>

// Async variants
const asyncSuccess = chas.okAsync(42);           // ResultAsync<number, never>
const asyncFailure = chas.errAsync('not found'); // ResultAsync<never, string>

Wrapping existing code

Use tryCatch to safely call any function that might throw:
const parsed = chas.tryCatch(
  () => JSON.parse(rawInput),
  (e) => `Invalid JSON: ${e}`
);
// Result<unknown, string>
Use fromPromise to wrap a Promise that might reject:
function fetchUser(id: string): chas.ResultAsync<User, string> {
  return chas.fromPromise(
    fetch(`/users/${id}`).then((r) => r.json()),
    (e) => `Failed to fetch user: ${e}`
  );
}

Chaining

All chaining methods skip the transformation when the result is an Err, so errors short-circuit automatically.

.map() and .mapErr()

Transform the value or the error without unwrapping:
const name = chas.ok({ name: 'Alice', age: 30 })
  .map((user) => user.name);
// Ok('Alice')

const louder = chas.err('oops')
  .mapErr((e) => e.toUpperCase());
// Err('OOPS')

.andThen()

Chain a function that itself returns a Result. Use this when the next step can also fail:
function parseAge(raw: string): Result<number, string> {
  const n = parseInt(raw, 10);
  return isNaN(n) ? chas.err('not a number') : chas.ok(n);
}

function validateAge(age: number): Result<number, string> {
  return age >= 18 ? chas.ok(age) : chas.err('must be 18+');
}

const result = parseAge('25').andThen(validateAge);
// Ok(25)

.orElse()

Recover from an error by returning a fallback Result:
const result = fetchUser('123')
  .orElse(() => chas.ok(GUEST_USER));
// If fetchUser fails, returns Ok(GUEST_USER)

Side effects

Use .tap() and .tapErr() to run side effects without modifying the result:
const result = await fetchUser('123')
  .tap((user) => console.log('Fetched:', user.name))
  .tapErr((err) => console.error('Error:', err));
// The result value is unchanged

Consuming results

.match()

The cleanest way to handle both branches:
const message = result.match({
  ok: (user) => `Hello, ${user.name}`,
  err: (e) => `Error: ${e}`,
});
// TypeScript infers the return type as string

Type narrowing with .isOk() / .isErr()

if (result.isOk()) {
  console.log(result.value); // value is typed as T
} else {
  console.error(result.error); // error is typed as E
}

.unwrap() and .unwrapOr()

.unwrap() throws if the result is an Err. Use it only when you are certain the result is Ok, or as a last resort:
const value = result.unwrapOr('default');      // returns T or default
const value = result.unwrapOrElse((e) => ''); // compute fallback from error
const value = result.unwrap();                 // throws if Err — use carefully
.unwrap() throws the contained error if the result is Err. Prefer .match(), .unwrapOr(), or type narrowing for safe access.

Attaching debug context

Use .context() to annotate errors with information about where in the chain the failure occurred. Context accumulates in order from most recent to oldest:
const result = await fetchUser(id)
  .context('fetching user')
  .andThen((u) => loadPermissions(u))
  .context('loading permissions');

if (result.isErr()) {
  console.log(result.error._context);
  // ['loading permissions', 'fetching user']
}

The built-in .pipe() method

Result has a .pipe() method that lets you pass the result through a sequence of functions:
const double = (r: Result<number, string>) => r.map((v) => v * 2);
const addTen = (r: Result<number, string>) => r.map((v) => v + 10);

const result = chas.ok(5).pipe(double, addTen);
// Ok(20)
.pipe() on a Result is different from the standalone pipe function; it receives the full Result, not just the inner value.

ResultAsync is awaitable

ResultAsync<T, E> implements PromiseLike, so you can await it directly to get a synchronous Result<T, E>:
const resultAsync = fetchUser('123'); // ResultAsync<User, string>

// Await to get a synchronous Result
const result = await resultAsync; // Result<User, string>

// Or chain further before awaiting
const name = await fetchUser('123')
  .map((u) => u.name)
  .mapErr((e) => e.toUpperCase());

Combining multiple results

chas.shapeAsync()

Combine several ResultAsync values into a single shaped object. If any input is an Err, the first error is returned:
const dashboardData = await chas.shapeAsync({
  user: fetchUser(id),         // ResultAsync<User, ApiError>
  posts: fetchPosts(id),       // ResultAsync<Post[], ApiError>
  settings: fetchSettings(id), // ResultAsync<Settings, ApiError>
});
// Result<{ user: User; posts: Post[]; settings: Settings }, ApiError>

if (dashboardData.isOk()) {
  const { user, posts, settings } = dashboardData.value;
}
const result = chas.shape({
  name: is.string.parse(data.name),
  age: is.number.parse(data.age),
});
// Result<{ name: string; age: number }, GuardErr>

Parallel combinators

// Succeeds if ALL succeed (short-circuits on first Err)
const all = await chas.allAsync([fetchUser('1'), fetchUser('2')]);
// Result<[User, User], ApiError>

// Collects ALL errors instead of short-circuiting
const collected = await chas.collectAsync([fetchUser('1'), fetchUser('2')]);
// Result<[User, User], ApiError[]>

// Succeeds with the first Ok, or Err with all errors
const any = await chas.anyAsync([fetchUser('1'), fetchUser('2')]);
// Result<User, ApiError[]>

// Resolves with whichever completes first
const race = await chas.raceAsync([fetchUser('1'), fetchUser('2')]);
// Result<User, ApiError>

Do-notation with chas.go()

Use chas.go() to write sequential Result-based code without deeply nested .andThen chains. Yielding an Err short-circuits immediately:
const profileResult = await chas.go(async function* () {
  const user = yield* fetchUserAsync(id);      // short-circuits if Err
  const perms = yield* loadPermissions(user);  // also short-circuits
  return { user, perms };
});
// ResultAsync<{ user: User; perms: Permissions }, ApiError>