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

ts-xerrorof

v0.2.0

Published

Tagged domain errors for TypeScript, with neverthrow Result helpers.

Readme

ts-xerrorof

Tagged domain errors for TypeScript. Define a factory per domain, get a typed union for free, and propagate the same shape whether you return it or throw it.

Requires Node.js (the log formatter uses util.inspect).

Install

pnpm add ts-xerrorof

Result (ok / err)

ok, err, Result, ResultAsync, and the other Result helpers are re-exported from neverthrow — not a rewrite. They are the same functions and types:

import { ok, err, type Result } from "ts-xerrorof";
import { ok as neverthrowOk } from "neverthrow";

ok === neverthrowOk; // true

Re-exported primitives: ok, err, Ok, Err, Result, okAsync, errAsync, ResultAsync, fromThrowable, fromAsyncThrowable, fromPromise, fromSafePromise, safeTry.

Use them with tagged errors:

import { defineErrors, err, ok, payload, type ErrorOf, type Result } from "ts-xerrorof";

function save(entity: Entity | null): Result<Entity, EntityError> {
  if (entity === null) {
    return err(EntityErr.validation("bad input"));
  }
  return ok(entity);
}

See the neverthrow docs for Result / ResultAsync methods (isOk, isErr, fromPromise, …).

Define a domain

import { defineErrors, payload, type ErrorOf } from "ts-xerrorof";

const EntityErr = defineErrors({
  validation: payload(), // { message }
  unauthorized: payload<{ status?: number }>(), // extra fields live on data
  network: payload(),
});

type EntityError = ErrorOf<typeof EntityErr>;

EntityErr.validation("bad input");
EntityErr.unauthorized("expired"); // data: {}
EntityErr.unauthorized("expired", { status: 401, cause });
// → { message, cause, data: { status: 401 } }

ErrorOf<typeof EntityErr> is the union of every variant. Narrow with isErrorOf — there is no public .type field on the error. formatError still includes the kind name in logs.

Attach a cause

Keep the caller message and attach the original failure as cause:

import { withCause } from "ts-xerrorof";

EntityErr.network(withCause("Entities request failed", cause));

Do not promote cause.message to the tagged message.

Narrow by builder identity

Each builder doubles as a variant tag. Matching is by the factory that minted the error, not the .type string, so EntityErr.network and ReferenceErr.network do not collide.

import { isErrorOf } from "ts-xerrorof";

if (isErrorOf(failure, EntityErr.unauthorized)) {
  failure.data.status; // `data` is required; `status` is optional on this payload
}

if (isErrorOf(failure, EntityErr.unauthorized, EntityErr.network)) {
  // OR check
}

isTaggedError is the unbranded guard (useful after a catch). Unbranded values accept any builder; runtime identity still decides.

isErrorOf only checks the error you pass. After withCause or remapError, the original lives on cause. hasErrorOf / findErrorOf walk that chain (including native Error.cause and AggregateError.errors). Matching is still by builder identity, so a remapped error can be searched for the collaborator variant.

import { findErrorOf, hasErrorOf } from "ts-xerrorof";

hasErrorOf(failure, EntityErr.unauthorized);

const unauthorized = findErrorOf(failure, EntityErr.unauthorized);
if (unauthorized) {
  unauthorized.data.status; // `data` is required after a match
}

hasErrorOf(processError, CaptureErr.not_found);

Remap across domains

isErrorOf will not treat CaptureErr.not_found as ProcessErr.not_found. When a layer should translate kind by name, use remapError. _ is the fallback. Message is kept; the original is cause.

import { remapError } from "ts-xerrorof";

return err(
  remapError(captureError, {
    not_found: ProcessErr.not_found,
    validation: ProcessErr.validation,
    _: ProcessErr.db,
  }),
);

Builders that require extra data fields are not assignable — map those by hand so you can pass status / body.

Return or throw

One tagged-error shape, two ways to propagate:

  • Return err(EntityErr.validation("bad input")) / ok(value) from services (Result from neverthrow).
  • Throw at a job-queue / framework boundary that catches Errors:
import { isTaggedError, toThrowable } from "ts-xerrorof";

throw toThrowable(failure);

try {
  await run();
} catch (caught) {
  if (isTaggedError(caught) && isErrorOf(caught, EntityErr.network)) {
    // ...
  }
}

toThrowable wraps the tag in a real Error (throw-site stack, native cause) and copies data and the brand so the catch can still narrow with isErrorOf.

Format for logs

import { formatError, formatTaggedError } from "ts-xerrorof";

console.error("upstream failed", formatError(failure));
// compact object: type, message, cause, data — no stack.
// Native errors also include name and Node detail fields (code, syscall, …).

formatTaggedError(failure);
// multi-line detail: message, type, data, stack, then the cause chain

formatError attaches a custom util.inspect so console.error (inspect depth 2) does not collapse nested causes.

License

MIT