JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 112
  • Score
    100M100P100Q76106F
  • License MIT

Lightweight Result type for error handling without exceptions

Package Exports

  • @zipbul/result

Readme

@zipbul/result

English | 한국어

npm coverage

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)

// With data — carry error details
const e2 = err('not found');
// e2.data → 'not found'

// 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

Immutability — every Err is Object.freeze()d. Attempting to modify properties in strict mode throws a TypeError.


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 true if value is a non-null object with the marker property set to true.
  • 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 E caveatisErr<E>() provides a type assertion only. It does not validate the shape of data at 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> = {
  data: E;
};

The marker property used for identification is deliberately excluded from the type. It is added internally by err() and checked by isErr() — 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 pathsafe(fn) detects a function via !(fn instanceof Promise). A function that returns a Promise is treated as sync — the Promise object becomes the success value T.

mapErr panic — if mapErr itself throws, the throw propagates (sync) or the returned promise rejects (async). This is by design — mapErr is user code, and its failure is a panic, not an Err.


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__'

ValidationsetMarkerKey() throws TypeError if the key is empty or whitespace-only.

Warning — changing the marker key means isErr() will no longer recognize Err objects 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 here

Chaining 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 standard if, 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' });
  }
}

🔌 Framework Integration Examples

Bun.serve
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,
});

📄 License

MIT