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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@frogfish/k2error

v2.0.1

Published

A simple error handling library for K2 applications.

Readme

@frogfish/k2error

Strict, RFC‑7807–compliant error contracts for Node/TypeScript. Explicit, grep‑friendly traces and a stable error taxonomy for consistent APIs.

Small, strict error library for building consistent APIs and services.

It centers around two ideas:

  • A well-defined set of service error types with stable HTTP mappings.
  • An explicit, source-typed trace string that you put in code and pass through layers, so you can grep across systems and find the exact throw site.

No magic. No auto-generation. You control the trace.

Why This Exists

When an error trickles down through multiple system layers (handlers → services → repositories → SDKs), the most useful anchor for debugging is a unique token that you can search for across all code and logs. Here, that token is a simple string you type into source right where the error originates. Because it is hard-coded, you can:

  • Grep your entire monorepo and satellite services for the token and land on its origin instantly.
  • Share the token in alerting, dashboards, and support tickets.
  • Keep stack traces private while still getting a precise breadcrumb.

Auto-generated traces defeat this: they’re random, ephemeral, and not searchable in code. This library intentionally requires an explicit trace string.

Features

  • ServiceError enum mapped to HTTP status codes.
  • K2Error with RFC 7807 Problem Details shape: { type, title, status, detail, trace, chain }.
  • Explicit, caller-provided trace string; never auto-generated.
  • Helpers:
    • assert, assertNotNull, invariant for concise validation.
    • wrap to normalize unknown errors into K2Error without losing your trace.
    • httpStatus to retrieve canonical HTTP code for a ServiceError.
    • isK2Error type guard.
  • ESM package with TypeScript declarations.

Install

npm i @frogfish/k2error

Quick Start

Create a human-meaningful, grep-friendly trace inline at the origin (use your password/ID generator, or a short mnemonic):

import { K2Error, ServiceError, assert } from "@frogfish/k2error";

export function createUser(input: { username?: string }) {
  assert(input.username, "Username required", "t-user-create-usernamerequired-001");
  // ... continue
}

Wrap unknown errors with the same trace so it’s searchable end-to-end:

import { wrap, ServiceError } from "@frogfish/k2error";

try {
  await db.insert(user);
} catch (err) {
  throw wrap(err, ServiceError.SERVICE_ERROR, "t-user-create-dbinsert-002", "Failed to persist user");
}

Mapping and Semantics

ServiceError values map to HTTP status codes. Key ones:

  • BAD_REQUEST, VALIDATION_ERROR, INVALID_REQUEST → 400
  • UNAUTHORIZED, INVALID_TOKEN, TOKEN_EXPIRED, AUTH_ERROR → 401
  • FORBIDDEN, INSUFFICIENT_SCOPE → 403
  • NOT_FOUND → 404
  • CONFLICT, ALREADY_EXISTS → 409
  • TOO_MANY_REQUESTS → 429
  • SYSTEM_ERROR, CONFIGURATION_ERROR, SERVICE_ERROR → 500
  • NOT_IMPLEMENTED → 501
  • BAD_GATEWAY → 502
  • SERVICE_UNAVAILABLE → 503
  • GATEWAY_TIMEOUT → 504

Notes:

  • ALREADY_EXISTS uses 409 (conflict).
  • SERVICE_ERROR is an internal/server error (500). Use BAD_GATEWAY (502) for upstream gateway failures.
  • Use UNAUTHORIZED (401) for authentication failures and FORBIDDEN (403) for authorization denials.

API

Imports

import {
  K2Error,
  ServiceError,
  assert,
  assertNotNull,
  invariant,
  wrap,
  chain,
  rethrow,
  attempt,
  attemptSync,
  attemptResult,
  httpStatus,
  isK2Error,
} from "@frogfish/k2error";

class K2Error

Constructor

new K2Error(error: ServiceError, errorDescription: string, trace: string, originalError?: unknown)

Properties

  • type: string – RFC 7807 type URI identifying the error type.
  • title: string – short, human-readable summary of the error type.
  • status: number – canonical HTTP status based on error.
  • detail: string – human-readable description (safe for clients).
  • trace: string – explicit token you typed in code.
  • chain: array – semantic hops with timestamps representing error propagation.
  • name: always "K2Error".
  • cause?: the original error/value when provided.

Methods

  • toJSON() → RFC 7807 problem+json: { type, title, status, detail, trace, chain } (safe for clients, no stack).
  • toDebugJSON() → like toJSON() but includes stack and normalized cause (for protected logs only).

function assert

assert(condition: unknown, errorDescription: string, trace: string, error?: ServiceError): asserts condition

Throws K2Error(error ?? VALIDATION_ERROR, ...) if condition is falsy. Use for concise guards and TS narrowing.

function assertNotNull

assertNotNull<T>(value: T | null | undefined, errorDescription: string, trace: string, error?: ServiceError): asserts value is T

Throws if value is null or undefined. Narrows to T on success.

function invariant

Alias of assert for stylistic preference.

function wrap

wrap(err: unknown, error?: ServiceError, trace: string, errorDescription?: string): K2Error

Normalizes unknown errors to K2Error. If err is already a K2Error, it is returned as-is (keeps its original trace). Otherwise a new K2Error is created using your provided trace and optional description.

function chain

chain(err: unknown, trace: string, errorDescription?: string, error?: ServiceError, stage?: string): K2Error

Adds a semantic hop to the error’s chain and returns a K2Error for rethrow. Optionally updates the surface error/code and error_description. The stage is a free-form label (e.g., "service:createUser", "repo:save").

function rethrow

rethrow(err: unknown, trace: string, errorDescription?: string, error?: ServiceError, stage?: string): never

Convenience wrapper around chain(...) that throws immediately.

function attempt (async)

attempt<T>(fn: () => Promise<T> | T, trace: string, errorDescription?: string, error?: ServiceError, stage?: string): Promise<T>

Runs fn and, on failure, rethrows as K2Error using chain(...). Replace many boundary try/catch blocks with this helper.

Example:

const user = await attempt(
  () => repo.save(input),
  "t-user-create-dbinsert-002",
  "Failed to persist user",
  ServiceError.SERVICE_ERROR,
  "repo:save"
);

function attemptSync (sync)

attemptSync<T>(fn: () => T, trace: string, errorDescription?: string, error?: ServiceError, stage?: string): T

Synchronous variant; useful for parsing/validation helpers.

Example:

const id = attemptSync(
  () => parseId(raw),
  "t-parse-id-001",
  "Invalid ID",
  ServiceError.VALIDATION_ERROR,
  "util:parseId"
);

function attemptResult (result-style)

attemptResult<T>(fn: () => Promise<T> | T, trace: string, errorDescription?: string, error?: ServiceError, stage?: string): Promise<{ ok: true; value: T } | { ok: false; error: K2Error }>

Never throws; returns a discriminated union. Handy in controllers to avoid try/catch.

Example:

const r = await attemptResult(
  () => http.get(url),
  "t-fetch-001",
  "Upstream request failed",
  ServiceError.BAD_GATEWAY,
  "sdk:get"
);
if (!r.ok) return res.status(r.error.status).type("application/problem+json").json(r.error.toJSON());
return res.json(r.value);

function httpStatus

httpStatus(error: ServiceError): number

Returns the canonical HTTP status for a ServiceError.

function isK2Error

isK2Error(e: unknown): e is K2Error

Type guard for values shaped like K2Error.

Patterns

1) Keep trace inline at origin

assert(input.items?.length, "At least one item required", "t-order-create-010");

Why: You can search this token in code and land exactly here.

2) Propagate the same trace through layers

const order = await attempt(
  () => repository.save(input),
  "t-order-create-db-011",
  "Failed to save order",
  ServiceError.SERVICE_ERROR,
  "repo:save"
);

Why: One token ties together logs from handler, service, and repository.

3) HTTP middleware (Express/Koa-style)

import { isK2Error } from "@frogfish/k2error";

function toResponse(err: unknown) {
  if (isK2Error(err)) {
    return {
      status: err.status,
      body: err.toJSON(),
    };
  }
  // Unknown: map to generic 500 without stack
  return {
    status: 500,
    body: {
      type: "about:blank",
      title: "Service Error",
      status: 500,
      detail: "An error occurred",
      trace: "t-Unknown-000", // optional: replace with a specific handler-level trace
    },
  };
}

When returning errors to HTTP clients, set Content-Type: application/problem+json for consistency with RFC 7807.

4) Authorization/Authentication

invariant(isAuthenticated, "Authentication required", "t-Auth-001", ServiceError.UNAUTHORIZED);
invariant(hasPermission, "Forbidden", "t-Auth-002", ServiceError.FORBIDDEN);

5) Serialization and Logging

logger.warn({ trace: err.trace, type: err.type, chain: err.chain }, err.detail);
res.type("application/problem+json").status(err.status).json(err.toPublicJSON());

Keep stacks out of client responses; use them only in protected logs.

Do’s and Don’ts

Do

  • Hard-code a readable trace string near the throw site.
  • Reuse the same trace as the error propagates.
  • Use assert/assertNotNull/invariant for concise checks.
  • Choose the most specific ServiceError to aid monitoring.

Don’t

  • Don’t auto-generate traces — they’re not grep-friendly in code.
  • Don’t leak stack traces to clients.
  • Don’t conflate auth (401) and permission (403) errors.

Express/Koa Middleware Examples

Express-style handler without try/catch using attemptResult:

import express from "express";
import { attemptResult, ServiceError, isK2Error } from "@frogfish/k2error";

const app = express();

app.post("/users", async (req, res) => {
  const r = await attemptResult(
    () => userService.create(req.body),
    "t-users-create-001",
    "Failed to create user",
    ServiceError.SERVICE_ERROR,
    "svc:createUser"
  );
  if (!r.ok) return res.type("application/problem+json").status(r.error.status).json(r.error.toJSON());
  res.status(201).json(r.value);
});

// Centralized error middleware — ensures only K2Error leaks
app.use((err: unknown, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
  if (isK2Error(err)) return res.type("application/problem+json").status(err.status).json(err.toJSON());
  // As a last resort, map unknowns to a safe response
  return res.type("application/problem+json").status(500).json({ type: "about:blank", title: "Service Error", status: 500, detail: "An error occurred", trace: "t-unknown-000" });
});

Koa-style example with attempt replacing try/catch:

import Koa from "koa";
import Router from "@koa/router";
import bodyParser from "koa-bodyparser";
import { attempt, isK2Error } from "@frogfish/k2error";

const app = new Koa();
const router = new Router();

router.post("/orders", async (ctx) => {
  ctx.body = await attempt(
    () => orderService.create(ctx.request.body),
    "t-orders-create-001",
    "Failed to create order",
    undefined,
    "svc:createOrder"
  );
  ctx.status = 201;
});

app.use(async (ctx, next) => {
  try { await next(); }
  catch (err) {
    if (isK2Error(err)) {
      ctx.status = err.status;
      ctx.type = "application/problem+json";
      ctx.body = err.toJSON();
    } else {
      ctx.status = 500;
      ctx.type = "application/problem+json";
      ctx.body = { type: "about:blank", title: "Service Error", status: 500, detail: "An error occurred", trace: "t-unknown-000" };
    }
  }
});

app.use(bodyParser());
app.use(router.routes());
app.use(router.allowedMethods());

ESLint Rule: Enforce Inline Trace Literals

To ensure traces remain inline literals (not variables), add a no-restricted-syntax rule. This example forbids non-literal third args to assert, assertNotNull, invariant, wrap, chain, rethrow, attempt, attemptSync, attemptResult:

{
  "rules": {
    "no-restricted-syntax": [
      "error",
      {
        "selector": "CallExpression[callee.name=/^(assert|assertNotNull|invariant|wrap|chain|rethrow|attempt|attemptSync|attemptResult)$/] > :matches(Identifier,MemberExpression,CallExpression,TemplateLiteral,ArrayExpression,ObjectExpression):nth-child(3)",
        "message": "Trace must be an inline string literal."
      },
      {
        "selector": "CallExpression[callee.name=/^(assert|assertNotNull|invariant|wrap|chain|rethrow|attempt|attemptSync|attemptResult)$/] Literal:nth-child(3)[regex(pattern, '.*', { flags: 'i' })]",
        "message": ""
      }
    ]
  }
}

If your ESLint doesn’t support complex selectors, a simpler alternative is a custom rule or a code review check. The intent: third argument must be a plain string literal.

TypeScript and Module Format

  • ESM package with "type": "module".
  • Ships .d.ts and .js under dist.
  • Works with modern Node runtimes and bundlers. If using CommonJS, import via dynamic import() or transpile.

License

GPL-3.0-only. See LICENSE.

Trace Tutorial

This library treats the trace as a deliberate, human-placed breadcrumb. Here’s a practical primer on how and why to use it.

Why traces

  • Precise origin: A static token lets you jump to the exact throw site by grepping the repo or using code search tools (e.g., ripgrep, Sourcegraph, OpenGrok, livegrep, GitHub code search).
  • Stable across layers: Reuse the same token to stitch logs from handler → service → repository → external SDK.
  • Low-noise in logs: Short, opaque strings avoid leaking internals while still enabling targeted debugging.

How to create traces

  • Use a password generator to produce a 20-character, lowercase alphanumeric string (a–z, 0–9). No special characters. Example: q8x1t4e0m6r9b1p7d2c3.
  • Optionally prefix with a short mnemonic for readability/context, still keeping it grep-friendly. Example: t-user-create-0q3x9b1l6k2v4y7m5c8.
  • Inline the trace literal directly at the assert/throw/wrap site. Do not store it in a variable. Do not auto-generate in code.

Step-by-step pattern

  1. Validate early with an inline assert literal:
assert(input.username, "Username required", "t-user-create-0q3x9b1l6k2v4y7m5c8");
  1. Wrap unknown errors using the same literal:
try {
  await repo.save(user);
} catch (err) {
  throw wrap(err, ServiceError.SERVICE_ERROR, "t-user-create-0q3x9b1l6k2v4y7m5c8", "Failed to save user");
}
  1. Log and return consistently:
logger.error({ trace: err.trace, type: err.type }, err.detail);
res.type("application/problem+json").status(err.status).json(err.toJSON());

Naming conventions

  • Keep tokens short and lowercase; avoid spaces/specials.
  • Prefer stable tokens per decision point; do not reuse across unrelated code paths.
  • Consider a light prefix for grouping (e.g., t-auth-..., t-order-...).

Anti-patterns

  • Auto-generating traces in code (not searchable in source, changes every run).
  • Overloading a single token for many unrelated failures.
  • Leaking stack traces to clients (use toPublicJSON() for responses).
  • For richer diagnostics in protected logs, use toDebugJSON().