okay-error

Typed, chain‑friendly, JSON‑safe Results for TypeScript
A small opinionated TypeScript library providing strongly-typed Result objects with chaining capabilities, inspired by Rust std::result.
Why okay-error?
- Plain object compatibility - an
Ok is { ok: true, value }, an Err is { ok: false, error }. Log it, persist it, send it over the wire.
- Type‑level errors - every possible failure is visible in the function signature (
Result<T, E>), not thrown from the shadows. Rely on the type checker to ensure you handle every possible failure.
- Cause‑chain built‑in - link any parent error using the
cause() helper; walk the cause links later to see the full logical call stack.
- Ergonomic - helpers
map, flatMap, or feel familiar to JS arrays.
- Re‑hydration - after
JSON.parse, call result to get a plain Result object.
Table of Contents
Install
npm i okay-error
Quick tour
From try-catch to Result
Here's how okay-error changes error handling from exceptions to data:
try {
const user = getUserById(123);
const greeting = formatGreeting(user.name);
console.log(greeting);
} catch (error) {
console.error('Something went wrong', error);
}
import { ok, err, result, annotate } from 'okay-error';
function getUserById(id: number) {
try {
if (id <= 0) {
return err('InvalidId', { id });
}
const user = { id, name: 'Ada' };
return ok(user);
} catch (error) {
return err('DbError', { cause: error });
}
}
const userResult = getUserById(123);
if (!userResult.ok) {
console.error(`Database error: ${userResult.error.type}`);
return;
}
const greeted = userResult
.map(u => u.name.toUpperCase())
.flatMap(name =>
name.startsWith('A')
? ok(`Hello ${name}!`)
: err('NameTooShort', { min: 1 })
)
.or('Hi stranger!');
console.log(greeted);
Propagating context
Context propagation allows you to wrap lower-level errors with higher-level context as they move up through your application's layers so you know where the error occurred.
function readConfig(): Result<string, ConfigErr> { }
function boot(): Result<void, BootErr> {
const cfg = readConfig();
if (!cfg.ok) {
return err('BootConfig', { phase: 'init', ...cause(cfg) });
}
return ok();
}
How cause works
cause creates a new object { cause: error } that can be spread into your error payload. This creates a discoverable, traceable error chain that's useful for debugging:
Err {
type: "BootConfig",
phase: "init",
cause: Err {
type: "ConfigFileMissing",
path: "/etc/app.json",
cause: Err { type: "IO", errno: "ENOENT" }
}
}
Working with async operations
okay-error can be used with async code to handle errors as data:
import { result } from 'okay-error';
async function fetchUserData(userId: string) {
const response = await result(fetch(`/api/users/${userId}`));
if (!response.ok) {
return annotate(response, 'NetworkError', { userId });
}
const data = await result(response.value.json());
if (!data.ok) {
return annotate(data, 'ParseError', { userId });
}
if (!data.value.name) {
return err('ValidationError', {
userId,
message: 'User name is required'
});
}
return ok(data.value);
}
async function displayUserProfile(userId: string) {
const userData = await fetchUserData(userId);
if (!userData.ok) {
switch (userData.error.type) {
case 'NetworkError':
console.error('Connection failed');
break;
case 'ParseError':
console.error('Invalid response format');
break;
case 'ValidationError':
console.error(userData.error.message);
break;
}
return;
}
console.log(`Welcome, ${userData.value.name}!`);
}
Feature checklist
| Typed constructors | err({ type: 'Timeout', ms: 2000 }) or err('Timeout', { ms: 2000 }) | |
map, flatMap, or | ok(1).map(x=>x+1).flatMap(fn).or(0) | |
| Works with Promise | await result(fetch(url)) | |
| Cause‑chain + optional stack frame | annotate(err(...), 'DB', {...}) | |
| JSON serialisable | JSON.stringify(err('X', {})) | |
| Re‑hydrate after JSON | const plain = result(JSON.parse(raw)) | |
API reference
Constructors
ok(value) | success result |
err(type, payload?) | typed error, payload is merged with { type } |
err({ ... }) | error from arbitrary value (object, string, etc) |
result(x) | wrap a sync fn, a Promise, or re‑hydrate a raw object |
Functions
cause(error) | wrap an error as a cause for another error |
match(result, { ok, err }) | pattern match on Result (success/failure) |
match(type, cases) | pattern match on a discriminant string (exhaustive) |
Types
type Result<T, E = unknown> = Ok<T> | Err<E>;
JSON round‑trip example
const errOut = err('DbConn', { host: 'db.local' });
const raw = JSON.stringify(errOut);
const back = result(JSON.parse(raw));
Error with cause example
import { err, cause } from 'okay-error';
const ioError = err('IO', { errno: 'ENOENT' });
const configError = err('ConfigFileMissing', { path: '/etc/app.json', ...cause(ioError) });
const bootError = err('BootConfig', { phase: 'init', ...cause(configError) });
const chained = err('BootConfig', cause(
err('ConfigFileMissing', cause(
err('IO', { errno: 'ENOENT' })
))
));
console.log(bootError.error.type);
console.log(bootError.error.cause.type);
The cause() helper
The cause(error) function is the idiomatic way to link any parent error as the cause of the current error—this parent could be a lower-level error, a related error, or any error that led to the current one:
const base = err('Base', { info: 123 })
const wrapped = err('Higher', { ...cause(base), context: 'extra' })
This is preferred over annotate, and is composable for deep error chains.
Pattern matching example
Pattern matching with match
The match function is overloaded:
- Use
match(result, { ok, err }) to branch on Result objects.
- Use
match(type, { ...cases }) to branch on discriminant string unions (exhaustive, type-safe).
matchType is now an alias for the discriminant string overload for backwards compatibility.
const result = divide(10, 2);
const message = match(result, {
ok: (value) => `Result: ${value}`,
err: (error) => `Error: ${error.message}`
});
console.log(result);
const errorResult = divide(10, 0).match({
ok: (value) => `Result: ${value}`,
err: (error) => `Error: ${error.message}`
});
console.log(errorResult);
Type Safety and Exhaustiveness
When using match with a discriminant string union, TypeScript will enforce exhaustiveness, ensuring you handle all possible cases. This provides an additional layer of type safety for error handling.
type ApiError =
| { type: 'NotFound'; id: string }
| { type: 'Timeout'; ms: number }
| { type: 'Unauthorized'; reason: string };
function fetchData(id: string): Result<{ name: string }, ApiError> {
}
const response = fetchData('slow');
if (!response.ok) {
const errorMessage = match(response.error.type, {
NotFound: () => `Item ${response.error.id} could not be found`,
Timeout: () => `Request timed out after ${response.error.ms}ms`,
Unauthorized: () => `Access denied: ${response.error.reason}`
});
console.log(errorMessage);
}
License
MIT