npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@zipbul/result

v0.1.4

Published

Lightweight Result type for error handling without exceptions

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)
// 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 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> = {
  stack: string;
  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' });
  }
}

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