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

@mypolis.eu/result

v0.7.2

Published

A lightweight, type-safe `Result<T, E>` type for TypeScript, inspired by Go's explicit error handling and Rust's Result type. Zero dependencies.

Readme

@mypolis.eu/result

A lightweight, type-safe Result<T, E> type for TypeScript, inspired by Go's explicit error handling and Rust's Result type. Zero dependencies.

Why?

TypeScript's try/catch doesn't tell you what can fail. Functions that throw hide their error types from the caller, and forgetting a try/catch leads to uncaught exceptions.

With Result, errors are just return values. The type system tracks them and forces you to handle both cases.

Installation

npm install @mypolis.eu/result

Core Types

Result<T, E>

type Result<T, E> = { ok: true; value: T; error: null } | { ok: false; value: null; error: E }

A discriminated union — check result.ok and TypeScript narrows the type automatically.

Error<K>

type Error<K extends string = "unknown-error", C = unknown> = {
	kind: K
	message?: string
	cause?: C
}

Define your domain errors as individual type aliases. Each error gets its own kind — no bundling into a single union type:

import { type Error } from "@mypolis.eu/result"

type AuthEmailInvalid       = Error<"auth-email-invalid">
type AuthEmailExists        = Error<"auth-email-exists">
type AuthTokenExpired       = Error<"auth-token-expired">
type AuthCodeMismatch       = Error<"auth-code-mismatch">
type AuthError              = Error<"auth-error">

Primary Pattern: Imperative Check and Return

This is the Go-inspired style — check for errors and propagate them explicitly. It's the recommended default for most code.

Scenario: User Registration

import { type Error, ok, err } from "@mypolis.eu/result"
import * as v from "valibot"

// Define domain errors
type AuthEmailInvalid = Error<"auth-email-invalid">
type AuthEmailExists  = Error<"auth-email-exists">
type AuthError        = Error<"auth-error">

// Validation layer: returns Result
function validateEmail(input: string): Result<string, AuthEmailInvalid> {
	const parsed = v.safeParse(v.pipe(v.string(), v.email()), input)
	if (!parsed.success) {
		return err({
			kind: "auth-email-invalid",
			message: `"${input}" is not a valid email`,
			cause: parsed.issues,
		})
	}
	return ok(parsed.output)
}

// Database layer: — returns Result via wrap
async function insertUser(email: string): Promise<Result<User, AuthEmailExists | AuthError>> {
	return wrapAsync(
		() => db.user.create({ data: { email } }),
		(cause) => {
			if (isDuplicateError(cause)) {
				return { kind: "auth-email-exists", message: "Email already registered", cause }
			}
			return { kind: "auth-error", message: "User creation failed", cause }
		},
	)
}

// Compose them: check and propagate
async function registerUser(email: string): Promise<Result<User, AuthEmailInvalid | AuthEmailExists | AuthError>> {
	const validEmail = validateEmail(email)
	if (!validEmail.ok) return validEmail

	const user = await insertUser(validEmail.value)
	if (!user.ok) return user

	return ok(user.value)
}

Key points:

  • Each function declares exactly what can go wrong in its return type
  • Error propagation is explicit: if (!result.ok) return result
  • The caller of registerUser can't forget to handle errors — the type enforces it
  • Unexpected errors (bugs) still throw — they aren't domain errors

match

match converts a Result into a concrete value by handling both branches. Use it at IO boundaries: HTTP handlers, CLI output, or anywhere you need a single return type.

// HTTP route handler
router.post("/register", async (req) => {
	const result = await registerUser(req.body.email)

	return match(result, {
		ok: (user) => new Response(JSON.stringify(user), { status: 201 }),
		err: (e) => {
			switch (e.kind) {
				case "auth-email-invalid":
					return new Response(JSON.stringify({ error: e.message }), { status: 400 })
				case "auth-email-exists":
					return new Response(JSON.stringify({ error: "Email already taken" }), { status: 409 })
				case "auth-error":
					return new Response(JSON.stringify({ error: "Service unavailable" }), { status: 502 })
			}
		},
	})
})
  • TypeScript enforces both branches return the same type (Response)
  • The switch on e.kind is exhaustive — add a new error variant and you get a compile error
  • No default case needed; the compiler proves all variants are handled

mapErr

When a low-level function returns technical errors, translate them into domain errors for the layer above:

type DbTimeout  = Error<"db-timeout">
type DbGone     = Error<"db-connection-lost">
type OrderError = Error<"order-unavailable">

// Low-level: returns technical errors
function findOrder(id: string): Result<Order, DbTimeout | DbGone>

// Service layer: translates to domain error
function getOrder(id: string): Result<Order, OrderError> {
	return mapErr(findOrder(id), (dbErr) => ({
		kind: "order-unavailable",
		message: "Could not retrieve order",
		cause: dbErr,
	}))
}

The happy path passes through untouched. Only the error is transformed.

wrap / wrapAsync

Use wrap to safely call functions that may throw — third-party libs, JSON.parse, fs operations, Valibot/Zod parsing without safeParse:

import { wrap, wrapAsync } from "@mypolis.eu/result"

// Synchronous: JSON.parse
function parseConfig(raw: string): Result<AppConfig, Error<"parse-error">> {
	return wrap(
		() => JSON.parse(raw) as AppConfig,
		(cause) => ({ kind: "parse-error", message: "Invalid config JSON", cause }),
	)
}

// Async: external API call
async function fetchRemoteConfig(): Promise<Result<AppConfig, Error<"fetch-error">>> {
	return wrapAsync(
		() => fetch("https://api.example.com/config").then((r) => r.json()),
		(cause) => ({ kind: "fetch-error", message: "Failed to fetch config", cause }),
	)
}

map

Less commonly needed. Extracts or reshapes a success value without leaving the Result:

// Without map
const sub = findActiveSubscription(userId)
if (!sub.ok) return sub
const plan = sub.value.plan

// With map — same thing in one line
const plan = map(findActiveSubscription(userId), (sub) => sub.plan)

Prefer the if-style when the transform is complex or when it improves readability.

unwrap

unwrap extracts the value or throws the error. Use it only where there's no caller to propagate to:

// CLI entry point — crash on failure
function main() {
	const config = unwrap(loadConfig())
	startServer(config)
}

// Tests — the assertion is the point
test("valid email passes validation", () => {
	const result = validateEmail("[email protected]")
	const email = unwrap(result)
	expect(email).toBe("[email protected]")
})

Passing a default value silences the error — avoid it in business logic:

// Bad — error is swallowed
const user = unwrap(findUser(id), { name: "Guest" })

// Good — caller decides how to recover
const result = findUser(id)
if (!result.ok) return redirect("/login")
const user = result.value

assertError — Testing

Asserts that a Result is an error with a specific kind. Narrows the type so the error variant is fully accessible:

import { assertError } from "@mypolis.eu/result"

test("duplicate email returns auth-email-exists", async () => {
	const result = await registerUser("[email protected]")
	assertError(result, "auth-email-exists")
	// result.error is narrowed to AuthEmailExists
	expect(result.error.message).toContain("already registered")
})

For the success case, use unwrap — it returns the value on success and throws (which fails the test) on error:

import { unwrap } from "@mypolis.eu/result"

test("valid registration succeeds", async () => {
	const result = await registerUser("[email protected]")
	const user = unwrap(result)
	expect(user.email).toBe("[email protected]")
})

Anti-Patterns

  • Don't throw inside Result-returning functions. If something is truly unexpected (bug, invariant violation), let it propagate naturally. Use wrap only at the boundary to call throw-based code.

  • Don't use unwrap with a default in business logic. It discards the error. Let the caller handle it explicitly.

  • Don't overuse combinators. If map or mapErr makes the code harder to read than an if-statement, use the if-statement.

License

MIT