@zipbul/result
v0.1.4
Published
Lightweight Result type for error handling without exceptions
Maintainers
Readme
@zipbul/result
English | 한국어
A lightweight Result type for error handling without exceptions.
Returns plain union values (T | Err<E>) instead of wrapping in classes — zero runtime overhead, full type safety.
No throw, no try/catch, no wrapper class. Just values.
📦 Installation
bun add @zipbul/result💡 Core Concept
Traditional error handling with throw breaks control flow, loses type information, and forces callers into a try/catch guessing game.
// ❌ Throw — caller has no idea what to expect
function parseConfig(raw: string): Config {
if (!raw) throw new Error('empty input'); // What type? Unknown.
if (!valid(raw)) throw new ValidationError(); // Silently propagates up.
return JSON.parse(raw);
}
try {
const config = parseConfig(input);
} catch (e) {
// What is `e`? Error? ValidationError? SyntaxError from JSON.parse?
// TypeScript cannot help you here — `e` is `unknown`.
}// ✅ Result — type-safe, explicit, no surprises
import { err, isErr, type Result } from '@zipbul/result';
function parseConfig(raw: string): Result<Config, string> {
if (!raw) return err('empty input');
if (!valid(raw)) return err('validation failed');
return JSON.parse(raw);
}
const result = parseConfig(input);
if (isErr(result)) {
console.error(result.data); // string — TypeScript knows the type
} else {
console.log(result.host); // Config — fully narrowed
}🚀 Quick Start
import { err, isErr, type Result } from '@zipbul/result';
interface User {
id: number;
name: string;
}
function findUser(id: number): Result<User, string> {
if (id <= 0) return err('Invalid ID');
const user = db.get(id);
if (!user) return err('User not found');
return user;
}
const result = findUser(42);
if (isErr(result)) {
// result is Err<string>
console.error(`Failed: ${result.data}`);
} else {
// result is User
console.log(`Hello, ${result.name}`);
}📚 API Reference
err()
Creates an immutable Err value. Never throws.
import { err } from '@zipbul/result';| Overload | Return | Description |
|:---------|:-------|:------------|
| err() | Err<never> | Error with no data |
| err<E>(data: E) | Err<E> | Error with attached data |
// No data — simple signal
const e1 = err();
// e1.data → never (cannot access)
// e1.stack → captured stack trace
// With data — carry error details
const e2 = err('not found');
// e2.data → 'not found'
// e2.stack → captured stack trace
// Rich error objects
const e3 = err({ code: 'TIMEOUT', retryAfter: 3000 });
// e3.data.code → 'TIMEOUT'Properties of the returned Err:
| Property | Type | Description |
|:---------|:-----|:------------|
| data | E | The attached error data |
| stack | string | Stack trace captured at err() call site |
Immutability — every
ErrisObject.freeze()d. Attempting to modify properties in strict mode throws aTypeError.
isErr()
Type guard that narrows a value to Err<E>.
import { isErr } from '@zipbul/result';function isErr<E = unknown>(value: unknown): value is Err<E>- Returns
trueifvalueis a non-null object with the marker property set totrue. - Never throws — handles
null,undefined, primitives, and exceptions internally.
const result: Result<number, string> = doSomething();
if (isErr(result)) {
// result: Err<string>
console.error(result.data);
} else {
// result: number
console.log(result + 1);
}Generic
Ecaveat —isErr<E>()provides a type assertion only. It does not validate the shape ofdataat runtime. Callers must ensure the generic matches the actual error type.
Result<T, E>
A plain union type — not a wrapper class.
type Result<T, E = never> = T | Err<E>;| Parameter | Default | Description |
|:----------|:--------|:------------|
| T | — | Success value type |
| E | never | Error data type |
// Simple — no error data
type MayFail = Result<Config>;
// With error data
type ParseResult = Result<Config, string>;
// Rich error types
type ApiResult = Result<User, { code: string; message: string }>;Err<E>
The error type returned by err().
type Err<E = never> = {
stack: string;
data: E;
};The marker property used for identification is deliberately excluded from the type. It is added internally by
err()and checked byisErr()— this keeps the public API surface clean and prevents consumers from depending on implementation details.
safe()
Wraps a sync function or Promise into a Result / ResultAsync. Catches throws and rejections, converting them to Err.
import { safe } from '@zipbul/result';| Overload | Return | Description |
|:---------|:-------|:------------|
| safe(fn) | Result<T, unknown> | Sync — calls fn(), catches throws |
| safe(fn, mapErr) | Result<T, E> | Sync — catches throws, maps via mapErr |
| safe(promise) | ResultAsync<T, unknown> | Async — wraps rejection |
| safe(promise, mapErr) | ResultAsync<T, E> | Async — wraps rejection, maps via mapErr |
// Sync — wrap a function that might throw
const result = safe(() => JSON.parse(rawJson));
if (isErr(result)) {
console.error('Parse failed:', result.data);
} else {
console.log(result); // parsed object
}
// Sync with mapErr — convert unknown throw to typed error
const typed = safe(
() => JSON.parse(rawJson),
(e) => ({ code: 'PARSE_ERROR', message: String(e) }),
);
// Async — wrap a Promise that might reject
const asyncResult = await safe(fetch('/api/data'));
// Async with mapErr
const apiResult = await safe(
fetch('/api/users/1'),
(e) => ({ code: 'NETWORK', message: String(e) }),
);Sync path —
safe(fn)detects a function via!(fn instanceof Promise). A function that returns a Promise is treated as sync — the Promise object becomes the success valueT.mapErr panic — if
mapErritself throws, the throw propagates (sync) or the returned promise rejects (async). This is by design —mapErris user code, and its failure is a panic, not anErr.
ResultAsync<T, E>
A type alias for async results — not a wrapper class.
type ResultAsync<T, E = never> = Promise<Result<T, E>>;| Parameter | Default | Description |
|:----------|:--------|:------------|
| T | — | Success value type |
| E | never | Error data type |
// Use as return type for async Result-returning functions
async function fetchUser(id: number): ResultAsync<User, string> {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) return err(res.statusText);
return await res.json();
}
// Or wrap an existing Promise with safe()
const result: ResultAsync<Response, string> = safe(
fetch('/api/data'),
(e) => String(e),
);Marker Key
The marker key is a unique hidden property used to identify Err objects. It defaults to a collision-resistant string.
import { DEFAULT_MARKER_KEY, getMarkerKey, setMarkerKey } from '@zipbul/result';| Export | Type | Description |
|:-------|:-----|:------------|
| DEFAULT_MARKER_KEY | string | '__$$e_9f4a1c7b__' — the default key |
| getMarkerKey() | () => string | Returns the current marker key |
| setMarkerKey(key) | (key: string) => void | Changes the marker key |
// Reset detection across independent modules
import { setMarkerKey, getMarkerKey } from '@zipbul/result';
setMarkerKey('__my_app_err__');
console.log(getMarkerKey()); // '__my_app_err__'Validation —
setMarkerKey()throwsTypeErrorif the key is empty or whitespace-only.Warning — changing the marker key means
isErr()will no longer recognizeErrobjects created with the previous key. Only change this if you need to isolate error domains across independent modules.
🔬 Advanced Usage
Result-returning functions
Define function signatures with Result to make error paths explicit in the type system.
import { err, isErr, type Result } from '@zipbul/result';
interface ValidationError {
field: string;
message: string;
}
function validate(input: unknown): Result<ValidData, ValidationError> {
if (!input || typeof input !== 'object') {
return err({ field: 'root', message: 'Expected an object' });
}
// ... validation logic
return input as ValidData;
}
const result = validate(body);
if (isErr(result)) {
return Response.json({ error: result.data }, { status: 400 });
}
// result is ValidData hereChaining results
Since Result is a plain union, there's no .map() or .flatMap(). Use standard control flow:
function processOrder(orderId: string): Result<Receipt, string> {
const order = findOrder(orderId);
if (isErr(order)) return order; // propagate
const payment = chargePayment(order);
if (isErr(payment)) return payment; // propagate
return generateReceipt(order, payment);
}This is intentional. Classes with
.map()/.flatMap()add runtime cost and force a specific composition style. Plain values +isErr()let you use standardif,switch, early return, and any other pattern you prefer.
Async results
Works naturally with Promise:
async function fetchUser(id: number): Promise<Result<User, ApiError>> {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) return err({ code: res.status, message: res.statusText });
return await res.json();
} catch {
return err({ code: 0, message: 'Network error' });
}
}Stack traces
Every Err captures a stack trace at creation time, enabling debugging without throw:
const e = err('something went wrong');
console.log(e.stack);
// Error
// at err (/.../err.ts:22:18)
// at validate (/.../validate.ts:15:12)
// at handleRequest (/.../server.ts:8:20)🔌 Framework Integration Examples
import { err, isErr, type Result } from '@zipbul/result';
interface AppError {
code: string;
message: string;
}
function parseBody(request: Request): Promise<Result<Payload, AppError>> {
// ... returns Result
}
Bun.serve({
async fetch(request) {
const body = await parseBody(request);
if (isErr(body)) {
return Response.json(
{ error: body.data.code, message: body.data.message },
{ status: 400 },
);
}
// body is Payload
return Response.json({ ok: true, data: process(body) });
},
port: 3000,
});import { Cors, CorsAction } from '@zipbul/cors';
import { isErr } from '@zipbul/result';
const corsResult = Cors.create({
origin: 'https://app.example.com',
credentials: true,
});
// Cors.create() returns Result<Cors, CorsError>
if (isErr(corsResult)) {
throw new Error(`CORS config error: ${corsResult.data.message}`);
}
const cors = corsResult;
// cors.handle() returns Promise<Result<CorsResult, CorsError>>
const result = await cors.handle(request);
if (isErr(result)) {
return new Response('Internal Error', { status: 500 });
}📄 License
MIT
