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;
}
Sync: chas.shape()
Async: chas.shapeAsync()
const result = chas.shape({
name: is.string.parse(data.name),
age: is.number.parse(data.age),
});
// Result<{ name: string; age: number }, GuardErr>
const result = await chas.shapeAsync({
user: fetchUser(id),
config: fetchConfig(),
});
// Result<{ user: User; config: Config }, ApiError>
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>