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

@nice-code/error

v0.58.0

Published

Readme

@nice-code/error

Docs: nicecode.io — guides, integrations, and the full API surface. Working with an AI assistant? Point it at nicecode.io/llms-error.txt (just this package) or nicecode.io/llms.txt (the whole stack) — the complete, current docs flattened into plain text.

Typed, serializable errors with domain hierarchies, pattern matching, and safe transport across API boundaries.

Install

bun add @nice-code/error

Core concepts

  • Domain — a named group of related errors (err_user_auth, err_payment)
  • Schema — maps error id strings to message, HTTP status, and optional typed context
  • Error id — identifies what went wrong within a domain; multiple ids can be active on one error
  • Context — structured data attached to an id (e.g. { username: string })
  • Hydration — deserializing context from a JSON round-trip back to the original typed value

Defining an error domain

import { defineNiceError, err } from "@nice-code/error";

export const err_auth = defineNiceError({
  domain: "err_auth",
  schema: {
    // No context — second arg to fromId is not accepted
    account_locked: err({
      message: "Account is locked",
      httpStatusCode: 403,
    }),

    // Required context — second arg to fromId is required
    invalid_credentials: err<{ username: string }>({
      message: ({ username }) => `Invalid credentials for: ${username}`,
      httpStatusCode: 401,
      context: { required: true },
    }),

    // Optional context
    rate_limited: err<{ retryAfter: number }>({
      message: (ctx) => ctx ? `Retry after ${ctx.retryAfter}s` : "Rate limited",
      httpStatusCode: 429,
      context: {},
    }),
  },
});

err() — schema entry helper

| Schema entry | fromId("id") | fromId("id", ctx) | |---|---|---| | err() / err({ message, httpStatusCode }) | ✓ | ✗ | | err<C>({ context: {} }) | ✓ optional | ✓ optional | | err<C>({ context: { required: true } }) | ✗ | ✓ required |

Custom serialization (non-JSON-safe context)

fs_error: err<{ cause: NodeJS.ErrnoException }>({
  message: ({ cause }) => `FS error: ${cause.message}`,
  context: {
    required: true,
    serialization: {
      toJsonSerializable: ({ cause }) => ({ code: cause.code, message: cause.message }),
      fromJsonSerializable: (obj) => ({ cause: Object.assign(new Error(obj.message), obj) }),
    },
  },
}),

Creating errors

// Single id — context required per schema
const err1 = err_auth.fromId("invalid_credentials", { username: "alice" });

// Multi-id in one error
const err2 = err_auth.fromContext({
  invalid_credentials: { username: "bob" },
  rate_limited: { retryAfter: 30 },
});

// Chain additional ids after construction
const err3 = err_auth.fromId("account_locked").addId("rate_limited");

Reading errors

// Check and narrow type
if (err1.hasId("invalid_credentials")) {
  const { username } = err1.getContext("invalid_credentials"); // typed
}

// Check multiple ids at once
if (err2.hasOneOfIds(["invalid_credentials", "rate_limited"])) {
  // ACTIVE_IDS narrowed to that subset
}

// Pattern match — calls first matching handler, returns its result
import { matchFirst } from "@nice-code/error";

const message = matchFirst(err1, {
  invalid_credentials: ({ username }) => `Wrong password for ${username}`,
  account_locked: () => "Account locked",
  _: () => "Unknown auth error",
});

Domain hierarchies

const err_app = defineNiceError({ domain: "err_app", schema: {} });

const err_auth = err_app.createChildDomain({
  domain: "err_auth",
  schema: { /* ... */ },
});

const err_auth_registration = err_auth.createChildDomain({
  domain: "err_auth_registration",
  schema: { /* ... */ },
});

// Ancestry checks
err_app.isParentOf(err_auth_registration);           // true
err_auth.isParentOf(err_auth_registration);          // true
err_auth_registration.isParentOf(err_auth);          // false

// Type guard — exact domain match only
err_auth.isExact(someError);         // true only for err_auth domain
err_auth.isThisOrChild(someError);   // true for err_auth and all children

Serialization & transport

// Serialize to a plain JSON object (safe to send over HTTP)
const json = error.toJsonObject();
const str = error.toJsonString();
const response = error.toHttpResponse(); // Response with correct HTTP status

// On the receiving side — reconstruct from anything
import { castNiceError } from "@nice-code/error";

const caught = castNiceError(unknownValue); // always returns NiceError

if (err_auth.isExact(caught)) {
  const hydrated = err_auth.hydrate(caught); // deserializes context
  const { username } = hydrated.getContext("invalid_credentials");
}

castNiceError handles: NiceError instances, serialized JSON objects, native Error, error-like objects, nullish values, and primitives.

Anything that wasn't already a NiceError becomes a generic wrapper with error.isUnhandled === true — "an error we never accounted for". Domain-defined errors (including ones reconstructed from JSON) keep isUnhandled === false. isUnhandled survives transport.

castAndHydrate — cast + narrow + hydrate in one call

The idiomatic way to handle an unknown value arriving from a remote boundary when you have a specific domain in mind:

import { castAndHydrate } from "@nice-code/error";

const error = castAndHydrate(unknownValue, err_auth);
// → NiceErrorHydrated when it belongs to err_auth, otherwise the raw cast NiceError

if (err_auth.isExact(error)) {
  const message = matchFirst(error, {
    invalid_credentials: ({ username }) => `Wrong password for ${username}`,
    account_locked: () => "Account locked",
  });
}

Error handling with handlers

Functional style (composable)

import { forDomain, forId, forIds } from "@nice-code/error";

const handled = error.handleWithSync([
  forId(err_auth, "invalid_credentials", (h) => {
    const { username } = h.getContext("invalid_credentials");
    return res.status(401).json({ error: `Bad credentials for ${username}` });
  }),
  forIds(err_auth, ["account_locked", "rate_limited"], (h) => {
    return res.status(h.httpStatusCode).json({ error: h.message });
  }),
  forDomain(err_payment, (h) => {
    return res.status(500).json({ error: "Payment failed" });
  }),
]);

// Async handlers
await error.handleWithAsync([
  forDomain(err_payment, async (h) => {
    await db.logFailure(h.message);
    await notify(h.toJsonObject());
  }),
]);

Class-based style (reusable handler)

import { NiceErrorHandler } from "@nice-code/error";

const handler = new NiceErrorHandler()
  .forId(err_auth, "invalid_credentials", (h) => "unauthorized")
  .forDomain(err_payment, (h) => "payment_error")
  .setDefaultHandler((e) => "unknown_error");

handler.handleErrorWithPromiseInspection(error);

Utility types

import type { InferNiceError, InferNiceErrorHydrated } from "@nice-code/error";

type TAuthError = InferNiceError<typeof err_auth>;
// → NiceError<{ domain: "err_auth"; ... }, keyof schema>

type TAuthErrorHydrated = InferNiceErrorHydrated<typeof err_auth>;

Packing for transport

Useful when errors must travel through systems that don't preserve custom properties (e.g. some RPC layers).

import { EErrorPackType } from "@nice-code/error";

error.pack(EErrorPackType.msg_pack);   // embeds JSON in error.message
error.pack(EErrorPackType.cause_pack); // embeds JSON in error.cause
error.unpack();                        // restore original

Running code that can fail — niceTry

Turn a throwing function into a typed result whose failure branch is always a NiceError.

import { niceTry, niceTryAsync, type TNiceResult } from "@nice-code/error";

const r = niceTry(() => JSON.parse(input));
if (!r.ok) report(r.error); // r.error is a NiceError (isUnhandled === true for a raw throw)
else use(r.output);

const r2 = await niceTryAsync(() => fetchUser(id)); // sync or async fn

TNiceResult<OUT, ERR extends NiceError = NiceError> is { ok: true; output } | { ok: false; error: ERR }. The catch runs through castNiceError, so declared domain errors pass through untouched and everything else becomes an isUnhandled wrapper.

Build results by hand with niceOk(output) / niceErr(error) — handy for functions that return a TNiceResult without going through niceTry:

import { niceOk, niceErr } from "@nice-code/error";

function parsePort(raw: string): TNiceResult<number> {
  const n = Number(raw);
  return Number.isInteger(n) ? niceOk(n) : niceErr(err_config.fromId("bad_port", { raw }));
}

Observability

import { onNiceError, setNiceErrorLogger } from "@nice-code/error";

// Tap every NiceError at creation — pipe to Sentry / OTel / devtools.
const off = onNiceError((error) => report(error.toStructuredLog()));

// Route the library's internal diagnostics through your own logger (default: console).
setNiceErrorLogger({ warn: myLogger.warn, debug: myLogger.debug, error: myLogger.error });

error.toStructuredLog() returns a flat, log-friendly object ({ domain, ids, message, httpStatusCode, isUnhandled, timeCreated, originError? }) — distinct from toJsonObject() (the wire form).


Sending across postMessage / structuredClone

A NiceError is an Error subclass, so structuredClone / postMessage drop its custom fields. Serialize before sending and cast on receipt — toJsonObject() is the single canonical wire form:

worker.postMessage(error.toJsonObject());      // sender
const error = castNiceError(event.data);       // receiver

Nested cause chains (and AggregateError.errors) are serialized to a bounded depth, JSON-safe.

To pattern-match an error that arrived over the wire, hydrate first (castAndHydrate), then matchFirst — a still-unhydrated context would otherwise throw.


Internal imports

The headline @nice-code/error surface is what most apps need. Lower-level helpers (wire-format inspection, the err_cast_not_nice fallback domain, isNiceErrorObject, context-state types) live behind an explicit subpath for libraries built on top of nice-error:

import { isNiceErrorObject, err_cast_not_nice } from "@nice-code/error/internal";