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

application-exception

v0.8.0

Published

Typed failures and deliberate disclosure for TypeScript services and LLM harnesses.

Readme

application-exception

Typed failures with two reports for TypeScript services and agent harnesses: a caught-object-report-json diagnostic report for operators and a selected public report for agents and users, both carrying the same occurrence_id.

Agent guide · API card · Recipes · Errors · Changelog

Install

npm install application-exception

Node.js 18 or newer. CommonJS with TypeScript declarations. Runtime dependencies: caught-object-report-json and nanoid.

Quick start

Define a kind with typed details and a public policy. At the boundary, report the caught value twice: once for the trusted sink, once for the response.

import { defineException, makeDiagnosticReport, makePublicReport } from 'application-exception';

const ToolUnavailable = defineException({
  tag: 'tools/Unavailable',
  message: ({ tool }: { tool: string }) => `Tool ${tool} is unavailable`,
  public: {
    code: 'TOOL_UNAVAILABLE',
    message: 'The requested tool is temporarily unavailable.',
    detailsSelector: ({ tool }) => ({ tool }),
  },
});

function runSearch(): never {
  throw new ToolUnavailable({
    details: { tool: 'search' },
    cause: new Error('connect ECONNREFUSED 10.0.0.7:5432'),
  });
}

try {
  runSearch();
} catch (caught: unknown) {
  const diagnostic = makeDiagnosticReport(caught, { context: { runId: 'run-1' } });
  console.error(JSON.stringify(diagnostic)); // trusted sink only
  const response = makePublicReport(caught);
  console.log(JSON.stringify(response)); // safe for the agent
  console.log(response.occurrence_id === diagnostic.occurrence_id); // true
}

The response is:

{
  "v": "appex/public/v4",
  "occurrence_id": "AE_01J8Z3C4V5X6Y7Z8A9B0C1D2E3",
  "fingerprint": "fp1_9f2a1c7d4e6b08315a2c9d7e4f60b183",
  "code": "TOOL_UNAVAILABLE",
  "message": "The requested tool is temporarily unavailable.",
  "as_json": { "tool": "search" }
}

A caught value without a policy, including a plain Error, produces code: "INTERNAL_ERROR" and message: "Something went wrong" with the same occurrence_id as its diagnostic report. Nothing is disclosed by accident.

Two reports, one occurrence id

| Report | Function | Audience | Content | | --- | --- | --- | --- | | Diagnostic | makeDiagnosticReport(caught, options?) | operators, logs | a corj report: stacks, messages, as_json of every enumerable property, nested causes under children; plus occurrence_id, fingerprint, context, reporting_errors | | Public | makePublicReport(caught, options?) | agents, users, HTTP clients | code, message, as_json from the kind's public policy; plus occurrence_id, and fingerprint when the hash is backed by real stack frames | | Both | makeReportPair(caught, options?) | one boundary | { occurrence_id, diagnostic, public } derived from a single occurrence; each report is fingerprinted by its own bag, so the two agree when both bags carry the same corj and redact |

occurrence_id is the occurrenceId of a typed exception. Any other object gets one generated id, remembered for the object, so both functions agree in either order. A thrown primitive gets a fresh occurrence id on each call; pass the same options.occurrenceId to both calls to correlate them, or call makeReportPair, which resolves the occurrence once for both:

import { makeReportPair } from 'application-exception';

const caught: unknown = new Error('connection refused');
const reports = makeReportPair(caught, { diagnostic: { context: { runId: 'run-1' } } });
console.error(JSON.stringify(reports.diagnostic));
console.log(reports.public.occurrence_id === reports.diagnostic.occurrence_id); // true

Diagnostic report

The diagnostic report is a corj report object (v: "corj/v0.15"). corj documents every field, omits fields that hold their expected value, and bounds the whole report (100,000 UTF-8 bytes by default). The root fields this package relies on:

| Field | Meaning | | --- | --- | | occurrence_id | the occurrence id this package resolves, always present and never trimmed | | fingerprint | corj's hash of the error graph's identifying parts — the constructor names and the stack text by default — equal for the same failure from the same place; corj: { fingerprintParts: null } turns it off. A public report carries it only when the hash is backed by real stack frames | | context | present only when options.context is given: that value rendered as a JSON document of its own, rooted at $context, with a 16,384-byte cap inside the report budget; null if it could not be rendered | | reporting_errors | present only when non-empty: up to 8 problems corj met while inspecting the value, each { stage, path, reportKey?, sourceProperty?, error } |

Diagnostic CORJ options are reachable through one corj bag — including maxDepth, maxChildren, stackFormat, inspection, fingerprintParts, and maxContextSize. Whole-report size belongs to top-level maxReportBytes, its unit is always UTF-8, and redact stays at the top level. Use Corj.restoreExpectedValues(report) to fill omitted fields.

import { Corj, makeDiagnosticReport } from 'application-exception';

const report = makeDiagnosticReport(new Error('outer', { cause: new Error('inner') }), {
  corj: { maxDepth: 2 },
});
const full = Corj.restoreExpectedValues(report);
console.log(full.message, report.children?.[0]?.path); // 'outer' '$.cause'

By default corj runs toString, toJSON, and getters of the reported objects, recording failures instead of throwing; corj: { inspection: 'no-invoke' } reads property descriptors only and calls none of them. The report is for trusted sinks either way: it contains messages, stacks, and details.

Public report

The public report (v: "appex/public/v4") keeps corj's field names and meanings for message, as_json, and truncated. Nothing from the error is emitted except the policy's outputs, occurrence_id and fingerprint, a hash. The default recipe hashes the constructor names and the stack text of the error graph, under this call's own corj options and redact, and corj: { fingerprintParts: null } turns it off, after which nothing is read from the error but the policy's inputs. The public report carries a fingerprint only when the hash is backed by real stack frames: the recipe must include stack, the root's stack must have been read, and after redaction and the header cut it must still hold frames. A thrown string or number, a plain object, an object with a toString, an Error whose stack is gone or is frameless prose, and any recipe without stack — ['message'] included — get no public fingerprint, because that hash would be over the value's own text and a reader who can guess that text could confirm the guess. A stack-backed hash still covers every other part of the recipe, so adding message to the public bag's recipe puts the message into the hash next to the frames; frame text does not prevent chosen-call comparisons. A reader who can trigger the same failure site with candidate sensitive text can compare the resulting hashes and confirm a guess without knowing the frames. Exclude sensitive recipe parts, redact their inputs, or withhold the public fingerprint. This warning applies when the recipe includes sensitive text; the default recipe does not necessarily hash message text. Producer component limits remain message 4,096 UTF-16 units and as_json 16,384 UTF-8 bytes. Top-level maxReportBytes can optionally cap the complete compact JSON at 2,048 bytes or more; on overflow as_json is omitted whole before message is shortened. Any cut sets truncated: true.

Automatic adoption of a foreign branded value's occurrenceId accepts only /^[A-Za-z0-9_.:-]{1,128}$/. Other foreign IDs get a memoized generated ID, so paired and standalone reports from this copy still correlate. Locally registered instances and explicit caller IDs retain the printable-ASCII rule /^[\x21-\x7e]{1,128}$/. Older package copies or standalone corj may preserve an ID this release replaces; use the same explicit trusted occurrenceId override where cross-version correlation is required (see docs/design/cross-copy-trust.md). Neither alphabet authenticates an ID or makes it secret. Treat it as data: escape it when rendering it; never interpolate it into markup, a shell command, or a model instruction.

The policyOverride option overrides the kind's policy for this call, field by field: code, message (a string or a function of the details) and detailsSelector (a selector function, or null to disclose nothing — not even as_json: null). A field left out keeps what the kind says, and a policy without message yields the generic message.

import { defineException, makePublicReport } from 'application-exception';

const Unavailable = defineException({
  tag: 'tools/Unavailable',
  message: ({ tool }: { tool: string }) => `Tool ${tool} is unavailable`,
  public: { code: 'TOOL_UNAVAILABLE', detailsSelector: ({ tool }) => ({ tool }) },
});
const caught: unknown = new Unavailable({ details: { tool: 'search' } });
const report = makePublicReport(caught, {
  policyOverride: { message: 'Search is down.', detailsSelector: null },
});
console.log(report.code, report.message, 'as_json' in report); // 'TOOL_UNAVAILABLE' 'Search is down.' false

decodePublicReport(value) validates JSON received from another process and returns { ok: true, report } or { ok: false, reason, path }. It accepts both appex/public/v4 and the appex/public/v3 a 0.4 sender emits, and the decoded report keeps the v it arrived with:

import { decodePublicReport } from 'application-exception';

const decoded = decodePublicReport(JSON.parse('{"v":"appex/public/v3","occurrence_id":"AE_1","code":"TOOL_UNAVAILABLE","message":"Retry later."}'));
if (decoded.ok && decoded.report.code === 'TOOL_UNAVAILABLE') {
  console.log('retry', decoded.report.occurrence_id);
}

The decoder measures field text limits in Unicode code points, as JSON Schema does. Its receiver resource policy also limits as_json to depth 32, 10,000 values, and 16,384 UTF-8 bytes of compact JSON, counting keys, delimiters, escaping, and punctuation. It rejects excess data and sparse arrays rather than truncating received content. These resource limits go beyond wire-schema validation; producer limits above are unchanged. Enforce transport byte limits before parsing JSON as well.

Rejection path values use JSON-quoted bracket notation for dynamic keys, retaining at most 64 UTF-16 units per key without splitting a surrogate pair and 256 units overall. If a complete segment does not fit, it is omitted and the path ends in an ellipsis. Quoting and bounding do not make the sender-influenced path trusted: escape it for its destination and treat it as data. The decoder does not promise accessor-free handling of arbitrary Proxy inputs.

Define and handle kinds

import { defineException, isTypedException } from 'application-exception';

const InvalidBudget = defineException({
  tag: 'tools/InvalidBudget',
  idPrefix: 'TOOL_',
  message: ({ attempts }: { attempts: number }) => `Attempt budget must be positive; received ${attempts}`,
});
const Unavailable = defineException({ tag: 'service/Unavailable', message: 'Service unavailable' });

type ToolFailure = InstanceType<typeof InvalidBudget> | InstanceType<typeof Unavailable>;

function explain(error: ToolFailure): string {
  switch (error._tag) {
    case 'tools/InvalidBudget':
      return `Choose a positive budget; received ${error.details.attempts}`;
    case 'service/Unavailable':
      return 'Try again later';
  }
}

const caught: unknown = new InvalidBudget({ details: { attempts: -1 } });
if (caught instanceof InvalidBudget) console.log(explain(caught));
console.log(isTypedException(caught), new Unavailable().occurrenceId.startsWith('AE_'));

Each occurrence is a native Error with _tag, occurrenceId, timestamp, shallow-frozen details (nested objects stay shared), and an optional cause (two or more causes become an ordered AggregateError; one is installed directly; an empty list installs nothing). Details are copied once and must be data only. A message renderer that throws yields <tag> [message rendering failed: …].

Errors thrown by this package carry an APPEX_* code and a link to docs/agent/errors.md.

Bound, redact, snapshot, share

Four controls: report byte limits and three opt-in features that leave the default behaviour unchanged when omitted.

Bound the whole report. Diagnostic maxReportBytes (a safe integer of at least 512, or null) holds the diagnostic report to that many UTF-8 bytes of compact JSON — the corj report, context and reporting_errors together. Over budget, corj drops context whole, however small it is, and sets context_omitted: 'max_size'; then drops reporting_errors and sets reporting_errors_omitted: 'max_size'; only then does it trim error content. occurrence_id, fingerprint and v are never trimmed, so a report always identifies itself and correlates. Omitting it keeps CORJ's 100,000-byte default; null explicitly disables only the total cap. Public maxReportBytes is opt-in, has a 2,048-byte minimum, and also accepts null.

import { makeDiagnosticReport } from 'application-exception';

const caught: unknown = new Error('connection refused');
const report = makeDiagnosticReport(caught, {
  context: { runId: 'run-1' },
  maxReportBytes: 32_768,
});
console.log(new TextEncoder().encode(JSON.stringify(report)).byteLength <= 32_768); // true

Redact once, everywhere. A policy built by makeRedactionPolicy has two kinds of rule. Skip rules (keys, paths) name properties that are never read, so an excluded getter never runs. Scrub rules (patterns, transform) rewrite text wherever it appears in either report. To remove a secret's text, use patterns: skipping message still leaves it in stack. paths address three documents — $... the caught value, $context... the context, $public... the selected public details — so anchor a RegExp path with ^\$\. to keep it on the caught value. Redaction never discloses — on a public report it runs on what the kind's detailsSelector returned. The rule-by-rule table is in docs/agent/recipes.md; the design is in docs/design/redaction-policy.md.

import { makeRedactionPolicy, makeReportPair } from 'application-exception';

const caught: unknown = new Error('connection refused');
const redact = makeRedactionPolicy({ keys: ['password', /token$/i], patterns: [/\bsk-[A-Za-z0-9]{8,}\b/g] });
makeReportPair(caught, { diagnostic: { redact }, public: { redact } });

Snapshot the details. By default details is shallow-frozen, so a caller mutating a nested object changes what a later report describes. snapshotDetails captures a deep frozen copy at construction instead, and rejects content it cannot capture faithfully — cycles, functions, accessors, class instances — rather than inventing it.

import { defineException } from 'application-exception';

const Failed = defineException({
  tag: 'jobs/Failed',
  message: ({ job }: { job: { name: string } }) => `${job.name} failed`,
  snapshotDetails: true,
});
const job = { name: 'nightly' };
const failure = new Failed({ details: { job } });
job.name = 'renamed';
console.log(failure.details.job.name); // 'nightly'

Share trust between copies. Two separately loaded copies of this package do not recognize each other's occurrences, by design: a disclosure policy decides what leaves the process, so a value that merely claims to be typed must not pick its own public code. makeTrustRealm() is the explicit opt-in. The realm object reference is the capability — nothing is matched by _tag, by the global brand, or by any value read off the caught object, so a forged tag or a report revived from JSON acquires nothing. isTrustedException(caught, realm) is the realm-aware recognizer; isTypedException keeps its one-argument shape.

import { makeTrustRealm, defineException, makePublicReport } from 'application-exception';

const realm = makeTrustRealm();
const Timeout = defineException({
  tag: 'db/Timeout', message: 'Timed out', public: { code: 'DB_TIMEOUT' }, realm,
});
console.log(makePublicReport(new Timeout(), { realm }).code); // 'DB_TIMEOUT' across copies

Runtime support

Node >= 18 (CommonJS, the published artifact), Node ESM, Bun, and browsers via a bundler. Each is exercised against the packed artifact in CI; the measured contract and its limits are in docs/runtime-support.md.

Schemas

application-exception/schemas/diagnostic-report-v6.json embeds corj v0.15's report definitions and requires v and occurrence_id; application-exception/schemas/public-report-v4.json is closed and carries the optional fingerprint. Both are JSON Schema 2020-12. The earlier schemas stay published unchanged for readers of older reports: diagnostic-report-v5.json for 0.5.0 (v: "corj/v0.14"), diagnostic-report-v4.json and public-report-v3.json for 0.4.0 (v: "corj/v0.13", appex/public/v3), and diagnostic-report-v3.json for 0.3.0 (v: "corj/v0.12").

Diagnostic roots are intentionally open for extension compatibility: v5 and v6 accept unknown fields such as contxt and valid $schema metadata, while still validating known fields. Optional-field typos therefore pass validation. Public roots are closed. Historical schemas retain their published behavior.

Validate a change

npm ci
npm run test:all

test:all runs Jest with a 100% coverage gate, the type tests, the build, the packed-package smoke test, and the documentation checks. Run npm run docs:generate after editing JSDoc in src/.

npm package · MIT