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

@valencets/resultkit

v0.2.0

Published

Opinionated, minimal Result monad for TypeScript. Railway-oriented programming with Ok, Err, map, andThen, match.

Downloads

383

Readme

resultkit

Opinionated, minimal Result monad for TypeScript. Railway-oriented error handling with Ok, Err, map, andThen, match — and nothing else you don't need.

Why resultkit?

TypeScript's try/catch is broken by design:

  • Caught errors are unknown — no type safety at the boundary
  • Errors are invisible in signatures — callers don't know a function can fail
  • Control flow jumps — thrown errors skip your logic and land wherever they want

Result monads fix this by encoding success and failure in the type system. Every fallible function returns Result<T, E> — the caller sees both paths and handles them explicitly.

Why not neverthrow?

neverthrow is good. resultkit exists because:

  • Smaller API surface. neverthrow ships safeTry, generators, andTee, orTee, andThrough, combine, combineWithAllErrors, iterator protocol, and more. resultkit ships only what you actually use.
  • Zero unsafe casts. No as unknown as anywhere in the implementation. Every type transformation creates a new instance with correct types.
  • Zero dependencies. Nothing in your node_modules you didn't ask for.
  • Extracted from production. Every method in resultkit exists because it was needed in a real codebase. Nothing speculative, nothing "just in case."

What's included

| Method | Description | |--------|-------------| | ok(value) | Create a success result | | err(error) | Create a failure result | | .isOk() / .isErr() | Type-narrowing guards | | .map(fn) | Transform the success value | | .mapErr(fn) | Transform the error value | | .andThen(fn) | Chain into another Result (flatMap) | | .orElse(fn) | Recover from an error | | .match(okFn, errFn) | Exhaustive pattern match | | .unwrapOr(default) | Extract value or use default | | .unwrap() / .unwrapErr() | Unsafe extraction (tests only) | | .toAsync() | Convert to ResultAsync | | fromThrowable(fn, errorFn?) | Wrap throwing functions at the boundary |

Plus ResultAsync<T, E> with the same API for async operations, and okAsync, errAsync, ResultAsync.fromPromise, ResultAsync.fromSafePromise constructors.

What's deliberately excluded

These features exist in other Result libraries but are not included here because they add complexity without demonstrated need:

  • safeTry / generator protocol
  • andTee / orTee / andThrough (side-effect methods)
  • combine / combineWithAllErrors
  • asyncMap / asyncAndThen
  • Iterator protocol

If you need them, use neverthrow. resultkit is for teams that want less, not more.

Install

npm install @valencets/resultkit

Quick Start

import { ok, err, fromThrowable } from '@valencets/resultkit'
import type { Result } from '@valencets/resultkit'

// Define your error type
interface AppError {
  readonly code: string
  readonly message: string
}

// Functions return Result instead of throwing
function divide (a: number, b: number): Result<number, AppError> {
  if (b === 0) {
    return err({ code: 'DIVISION_BY_ZERO', message: 'Cannot divide by zero' })
  }
  return ok(a / b)
}

// Chain operations — errors short-circuit automatically
const result = divide(100, 5)
  .andThen((n) => divide(n, 2))
  .map((n) => n.toFixed(2))

// Handle both paths explicitly
result.match(
  (value) => console.log(`Result: ${value}`),
  (error) => console.error(`Error: ${error.message}`)
)

Usage

Sync Results

import { ok, err } from '@valencets/resultkit'
import type { Result } from '@valencets/resultkit'

// Construction
const success = ok(42)          // Ok<number, never>
const failure = err('oops')     // Err<never, string>

// Type narrowing
function handle (result: Result<number, string>) {
  if (result.isOk()) {
    console.log(result.value)   // TypeScript knows .value exists
  }
  if (result.isErr()) {
    console.log(result.error)   // TypeScript knows .error exists
  }
}

// Transforms
ok(5)
  .map((n) => n * 2)           // Ok(10)
  .andThen((n) =>
    n > 100 ? err('too big') : ok(n)
  )                             // Ok(10)
  .mapErr((e) => e.toUpperCase()) // passes through Ok unchanged
  .match(
    (value) => `got ${value}`,
    (error) => `failed: ${error}`
  )                             // 'got 10'

// Recovery
err('bad')
  .orElse(() => ok(0))         // Ok(0) — recovered from error

// Default values
err('bad').unwrapOr(42)        // 42
ok(5).unwrapOr(42)             // 5

Async Results

import { okAsync, errAsync, ResultAsync } from '@valencets/resultkit'

// Wrap promises that might reject
const fetchUser = (id: number) =>
  ResultAsync.fromPromise(
    fetch(`/api/users/${id}`).then((r) => r.json()),
    (e) => ({ code: 'FETCH_FAILED', message: String(e) })
  )

// Chain async operations
const userName = await fetchUser(1)
  .map((user) => user.name)
  .andThen((name) =>
    name.length > 0 ? okAsync(name) : errAsync({ code: 'INVALID', message: 'empty name' })
  )
  .match(
    (name) => name,
    (error) => `Unknown (${error.code})`
  )

// Wrap safe promises (guaranteed not to reject)
const config = ResultAsync.fromSafePromise(
  fs.readFile('./config.json', 'utf-8')
)

// Convert sync to async
const asyncResult = ok(5).toAsync()  // ResultAsync<number, never>

Wrapping Throwing Code

import { fromThrowable } from '@valencets/resultkit'

// Wrap any function that might throw
const safeJsonParse = fromThrowable(
  (s: string) => JSON.parse(s),
  (e) => ({ code: 'PARSE_FAILED', message: e instanceof Error ? e.message : 'unknown' })
)

safeJsonParse('{"valid": true}')  // Ok({ valid: true })
safeJsonParse('not json')         // Err({ code: 'PARSE_FAILED', message: '...' })

// Without errorFn — error type is unknown
const safeParse = fromThrowable(JSON.parse)
safeParse('bad')  // Result<any, unknown>

Railway-Oriented Programming

Chain operations and let errors propagate automatically:

import { ok, err, ResultAsync } from '@valencets/resultkit'
import type { Result } from '@valencets/resultkit'

interface DbError { readonly code: string; readonly message: string }
interface User { readonly id: number; readonly name: string; readonly email: string }

function validateEmail (email: string): Result<string, DbError> {
  return email.includes('@')
    ? ok(email)
    : err({ code: 'INVALID_EMAIL', message: 'Email must contain @' })
}

function findUser (email: string): ResultAsync<User, DbError> {
  return ResultAsync.fromPromise(
    db.query('SELECT * FROM users WHERE email = $1', [email]),
    (e) => ({ code: 'QUERY_FAILED', message: String(e) })
  )
}

function sendWelcome (user: User): ResultAsync<void, DbError> {
  return ResultAsync.fromPromise(
    mailer.send(user.email, 'Welcome!'),
    (e) => ({ code: 'MAIL_FAILED', message: String(e) })
  )
}

// Each step either succeeds and continues, or fails and short-circuits
const result = validateEmail(input)
  .toAsync()
  .andThen((email) => findUser(email))
  .andThen((user) => sendWelcome(user))

await result.match(
  () => console.log('Welcome email sent'),
  (error) => console.error(`Failed at ${error.code}: ${error.message}`)
)

Type Inference Helpers

import type { InferOkType, InferErrType, Result } from '@valencets/resultkit'

type MyResult = Result<number, string>

type T = InferOkType<MyResult>   // number
type E = InferErrType<MyResult>  // string

API Reference

Constructors

| Function | Signature | Description | |----------|-----------|-------------| | ok | ok<T>(value: T): Ok<T, never> | Create a success result | | err | err<E>(error: E): Err<never, E> | Create a failure result | | okAsync | okAsync<T>(value: T): ResultAsync<T, never> | Create an async success | | errAsync | errAsync<E>(error: E): ResultAsync<never, E> | Create an async failure | | fromThrowable | fromThrowable(fn, errorFn?): (...args) => Result | Wrap a throwing function | | ResultAsync.fromPromise | fromPromise(promise, errorFn): ResultAsync | Wrap a potentially-rejecting promise | | ResultAsync.fromSafePromise | fromSafePromise(promise): ResultAsync | Wrap a promise that never rejects |

Instance Methods

All methods are available on both Result and ResultAsync (async versions return Promise from terminal methods).

| Method | On Ok | On Err | |--------|-------|--------| | .isOk() | true (narrows type) | false | | .isErr() | false | true (narrows type) | | .map(fn) | Applies fn to value | Passes through unchanged | | .mapErr(fn) | Passes through unchanged | Applies fn to error | | .andThen(fn) | Applies fn (must return Result) | Passes through unchanged | | .orElse(fn) | Passes through unchanged | Applies fn (must return Result) | | .match(okFn, errFn) | Calls okFn(value) | Calls errFn(error) | | .unwrapOr(default) | Returns value | Returns default | | .unwrap() | Returns value | Throws | | .unwrapErr() | Throws | Returns error | | .toAsync() | Converts to ResultAsync | Converts to ResultAsync |

Design Principles

  1. Explicit over implicit. Every error path is visible in the type signature.
  2. Minimal API. If we haven't needed it in production, it's not here.
  3. Zero dependencies. Your supply chain is your attack surface.
  4. Type safety without casts. No as unknown as anywhere in the implementation.
  5. Boundary isolation. try/catch exists in exactly one place: fromThrowable.

Contracts

.map() and .mapErr() mappers must not throw. These methods are for infallible transforms. If a mapper throws (sync) or rejects (async), the exception propagates as an unhandled rejection rather than being captured as an Err. Use .andThen() with fromThrowable for fallible operations.

fromThrowable errorFn must not throw. The errorFn mapper runs inside the catch block. If errorFn itself throws, that exception escapes the boundary. Keep errorFn pure — simple object construction like (e) => ({ code: 'FAILED', message: String(e) }).

ResultAsync.fromSafePromise callers guarantee the promise never rejects. If the promise rejects, the rejection propagates as an unhandled rejection rather than being captured as an Err. Use ResultAsync.fromPromise with an errorFn for promises that might reject.

Requirements

  • Node.js >= 22
  • TypeScript >= 5.9 (for best type inference)
  • ESM only ("type": "module")

License

MIT