Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 1.0.0

- Explain This Failure: real errors and cause chains.
- Typed API, CLI integration, runnable codebase example and meaningful workflow tests.
- Stable contracts and migration guidance; original humor stays around accurate facts.

## 0.4.0

46 built-in definitions cover filesystem, permissions, networking, module loading, streams, HTTP response lifecycle and JavaScript errors. Each has a specific explanation, likely causes and practical checks. Rubber-duck commentary has a distinct original line for every supported definition. No commands run automatically and unknown errors remain explicitly unrecognized.
Expand Down
6 changes: 6 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,9 @@ Install 0.3.0 with npm. Seeds and exact humorous wording are version-specific. D
46 distinct error definitions with explanations, causes and practical checks; original duck commentary for each; structured errorCatalog API and --catalog CLI.

Default behavior is retained except that the expanded error catalog now recognizes additional errors. New rotation and release-plan features are opt-in.

## 0.4.0 to stable 1.0.0

diagnoseError/renderDiagnosis and diagnostic CLI flags are additive. Explicit code precedence and existing catalog/batch behavior remain. Use diagnosis.original to hand the same error to your existing handler; serializing a diagnosis omits that reference.

See README for exact contracts, bounds and failure behavior.
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,35 @@
# error-translator

## Explain This Failure: real errors and cause chains (1.0.0)

diagnoseError(error, options) walks actual Error.cause chains with a default eight-layer bound (maxDepth 1–32). It detects cycles, exposes unknown causes honestly and returns structured chain entries with a translation for each layer. It never throws away the original error: result.original is the exact input reference and is deliberately non-enumerable so JSON output excludes it. The caller controls rethrowing, logging and exit behavior.

```js
import { diagnoseError, renderDiagnosis } from "@deployanyway/error-translator";
try {
await loadConfig();
} catch (error) {
const diagnosis = diagnoseError(error, { mode: "rubber-duck" });
console.error(renderDiagnosis(diagnosis));
throw error; // preserve failure behavior and the original stack
}
```

Options: mode plain/rubber-duck, maxDepth, includeStack (default false). Entries contain depth, name, message, optional code, translation and opt-in stack. stopped is complete/cycle/depth-limit; truncated reports incomplete traversal. Arbitrary primitive causes receive an explicit unknown translation. The caller's errors and causes are not mutated. AggregateError.errors is not flattened; this API follows the cause chain.

```sh
printf '%s' '{"message":"Config failed","cause":{"code":"ENOENT","message":"open config.json"}}' | error-translator --diagnose --json
node node_modules/@deployanyway/error-translator/examples/explain-failure.mjs ./missing-config.json
```

--diagnose accepts a JSON error record from stdin or a quoted argument, with --max-depth and opt-in --include-stack. It rejects batch/catalog/list combinations. Translation/diagnosis exits 0; invalid input exits 2. The runnable filesystem example preserves application failure with exit 1. Messages and opt-in stacks can contain private data; this library does not redact them or run fixes.

## Stable v1 contract

Node 22.13+ or Node 24. MIT licensed. CLI flags, structured fields, ESM/CommonJS exports and declarations are covered by tests and installed-package checks. Existing 0.4 APIs remain available except the explicitly documented doggo-log redaction/text-context changes. Future incompatible public API changes require a major release; callers should consume structured fields rather than parse jokes. Exact humorous wording and seeded catalog choices are version-specific. No telemetry, external API keys or network service is needed for core use.

Run npm test, npm run lint, npm run format:check, npm run coverage, npm run test:types and npm run verify:package from a source checkout. Runnable examples are shipped under examples/. The root demo is https://deployanyway.github.io/.

## Browse the debugging library (0.4.0)

46 built-in definitions cover filesystem, permissions, networking, module loading, streams, HTTP response lifecycle and JavaScript errors. Each has a specific explanation, likely causes and practical checks. Rubber-duck commentary has a distinct original line for every supported definition. No commands run automatically and unknown errors remain explicitly unrecognized.
Expand Down
12 changes: 7 additions & 5 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# 0.4.0 — 2026-10-08
# 1.0.0 — Explain This Failure: real errors and cause chains

46 distinct error definitions with explanations, causes and practical checks; original duck commentary for each; structured errorCatalog API and --catalog CLI.
Explain real Node.js errors and cause chains with practical debugging checks. The stack trace has chosen violence; the duck brought context.

Requires Node 22.13+ or 24. See README for copyable CLI and API examples. All required source, type, coverage and installed archive checks passed locally; CI must pass on the final PR head before merging. Original content, MIT code, no backend calls from the libraries.
diagnoseError/renderDiagnosis and diagnostic CLI flags are additive. Explicit code precedence and existing catalog/batch behavior remain. Use diagnosis.original to hand the same error to your existing handler; serializing a diagnosis omits that reference.

## Compatibility
Install: `npm install @deployanyway/error-translator@1.0.0`

Existing APIs remain available; new modes are explicit. Expanded error matching may recognize codes that were previously unknown.
See README for runnable API/CLI examples, supported formats, defaults and limitations. Existing catalogs remain. Core APIs require no online services. Root demo: https://deployanyway.github.io/.

Validation is recorded in the v1 release report after final CI and installed-package verification.
11 changes: 11 additions & 0 deletions examples/explain-failure.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { readFileSync } from "node:fs";
import { diagnoseError, renderDiagnosis } from "@deployanyway/error-translator";
const file = process.argv[2] ?? "missing-config.json";
try {
readFileSync(file, "utf8");
console.log("Config read successfully.");
} catch (cause) {
const failure = new Error("Application config could not be read", { cause });
console.error(renderDiagnosis(diagnoseError(failure)));
process.exitCode = 1;
}
33 changes: 31 additions & 2 deletions index.d.cts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
export type ErrorInput =
string | Error | { code?: string; name?: string; message?: string };
export interface ErrorRecord {
code?: string;
name?: string;
message?: string;
stack?: string;
cause?: unknown;
}
export type ErrorInput = string | Error | ErrorRecord;
export interface TranslationOptions {
mode?: "plain" | "rubber-duck";
}
Expand All @@ -23,3 +29,26 @@ export function renderTranslation(result: Translation): string;
export function listErrors(): string[];

export function errorCatalog(options?: TranslationOptions): Translation[];
export interface DiagnosisOptions extends TranslationOptions {
maxDepth?: number;
includeStack?: boolean;
}
export interface DiagnosticLayer {
depth: number;
name: string;
message: string;
code?: string;
translation: Translation;
stack?: string;
}
export interface Diagnosis {
readonly original: ErrorInput;
chain: DiagnosticLayer[];
stopped: "complete" | "cycle" | "depth-limit";
truncated: boolean;
}
export function diagnoseError(
error: ErrorInput,
options?: DiagnosisOptions,
): Diagnosis;
export function renderDiagnosis(result: Diagnosis): string;
33 changes: 31 additions & 2 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
export type ErrorInput =
string | Error | { code?: string; name?: string; message?: string };
export interface ErrorRecord {
code?: string;
name?: string;
message?: string;
stack?: string;
cause?: unknown;
}
export type ErrorInput = string | Error | ErrorRecord;
export interface TranslationOptions {
mode?: "plain" | "rubber-duck";
}
Expand All @@ -23,3 +29,26 @@ export function renderTranslation(result: Translation): string;
export function listErrors(): string[];

export function errorCatalog(options?: TranslationOptions): Translation[];
export interface DiagnosisOptions extends TranslationOptions {
maxDepth?: number;
includeStack?: boolean;
}
export interface DiagnosticLayer {
depth: number;
name: string;
message: string;
code?: string;
translation: Translation;
stack?: string;
}
export interface Diagnosis {
readonly original: ErrorInput;
chain: DiagnosticLayer[];
stopped: "complete" | "cycle" | "depth-limit";
truncated: boolean;
}
export function diagnoseError(
error: ErrorInput,
options?: DiagnosisOptions,
): Diagnosis;
export function renderDiagnosis(result: Diagnosis): string;
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 7 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@deployanyway/error-translator",
"version": "0.4.0",
"description": "Plain-English Node.js error explanations and debugging tips, because the stack trace has chosen violence.",
"version": "1.0.0",
"description": "Explain real Node.js errors and cause chains with practical debugging checks. The stack trace has chosen violence; the duck brought context.",
"type": "module",
"exports": {
".": {
Expand All @@ -27,7 +27,8 @@
"dist",
"index.d.ts",
"index.d.cts",
"MIGRATION.md"
"MIGRATION.md",
"examples"
],
"engines": {
"node": ">=22.13"
Expand Down Expand Up @@ -74,7 +75,9 @@
"plain-english",
"typescript",
"humor",
"deployanyway"
"deployanyway",
"error-cause",
"error-handling"
],
"main": "./dist/index.cjs",
"types": "./index.d.ts",
Expand Down
22 changes: 22 additions & 0 deletions scripts/verify-package.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,28 @@ try {
],
temp,
);
assert.ok(
pack.files.some((f) => f.path === "examples/explain-failure.mjs"),
"Runnable example must ship",
);
assert.equal(
run(
[
"--input-type=module",
"-e",
"import * as api from '@deployanyway/error-translator';const error=new Error('Wrapped',{cause:Object.assign(new Error('Missing file'),{code:'ENOENT'})}); const r=api.diagnoseError(error); if(r.original!==error||r.chain[1].code!=='ENOENT')throw new Error('Cause lost'); console.log(JSON.stringify(r));",
],
temp,
),
run(
[
"--input-type=commonjs",
"-e",
"const api=require('@deployanyway/error-translator');const error=new Error('Wrapped',{cause:Object.assign(new Error('Missing file'),{code:'ENOENT'})}); const r=api.diagnoseError(error); if(r.original!==error||r.chain[1].code!=='ENOENT')throw new Error('Cause lost'); console.log(JSON.stringify(r));",
],
temp,
),
);
console.log(
JSON.stringify(
{
Expand Down
25 changes: 24 additions & 1 deletion src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
translateErrors,
listErrors,
errorCatalog,
diagnoseError,
renderDiagnosis,
} from "./index.js";

import { readStdin } from "./input.js";
Expand All @@ -21,19 +23,40 @@ try {
batch: { type: "boolean" },
list: { type: "boolean" },
catalog: { type: "boolean" },
diagnose: { type: "boolean" },
"include-stack": { type: "boolean" },
"max-depth": { type: "string" },
mode: { type: "string", default: "plain" },
},
});
if (values.help) {
console.log(
'Usage: error-translator <error code or quoted message> [--json] [--mode plain|rubber-duck]\n\nTranslate errors into human-readable guidance. Without arguments, read stdin (256 KiB). --batch reads a JSON array of 1–100 errors. --list lists supported codes. --catalog prints all explanations and checks.\n\nOptions:\n -h, --help Show help\n -v, --version Show version\n --json Print a structured JSON result\n --mode plain|rubber-duck Keep useful guidance; add duck commentary\n\nExamples:\n error-translator ECONNREFUSED\n error-translator "TypeError: value is not a function" --json\n\nExit codes: 0 translation/help/version; 2 invalid arguments.',
'Usage: error-translator <error code or quoted message> [--json] [--mode plain|rubber-duck]\n\nTranslate errors into human-readable guidance. --diagnose reads a JSON error/cause record; --max-depth 1..32 bounds it; --include-stack opts into original stacks. Without arguments, read stdin (256 KiB). --batch reads a JSON array of 1–100 errors. --list lists supported codes. --catalog prints all explanations and checks.\n\nOptions:\n -h, --help Show help\n -v, --version Show version\n --json Print a structured JSON result\n --mode plain|rubber-duck Keep useful guidance; add duck commentary\n\nExamples:\n error-translator ECONNREFUSED\n error-translator "TypeError: value is not a function" --json\n\nExit codes: 0 translation/help/version; 2 invalid arguments.',
);
} else if (values.version) {
console.log(
JSON.parse(
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
).version,
);
} else if (values.diagnose) {
if (values.batch || values.catalog || values.list)
throw new TypeError(
"--diagnose cannot combine with --batch, --catalog or --list.",
);
const text = positionals.length ? positionals.join(" ") : await readStdin();
const result = diagnoseError(JSON.parse(text), {
mode: values.mode,
includeStack: values["include-stack"] ?? false,
...(values["max-depth"] !== undefined
? { maxDepth: Number(values["max-depth"]) }
: {}),
});
console.log(
values.json ? JSON.stringify(result, null, 2) : renderDiagnosis(result),
);
} else if (values["include-stack"] || values["max-depth"] !== undefined) {
throw new TypeError("--include-stack and --max-depth require --diagnose.");
} else if (values.catalog) {
if (positionals.length || values.batch || values.list)
throw new TypeError(
Expand Down
79 changes: 79 additions & 0 deletions src/diagnostics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { translateError, renderTranslation } from "./index.js";

/** Snapshot a bounded cause chain; preserve the original without serializing it. */
export function diagnoseError(error, options = {}) {
if (!options || typeof options !== "object" || Array.isArray(options))
throw new TypeError("Options must be an object.");
const maxDepth = options.maxDepth ?? 8,
includeStack = options.includeStack ?? false;
if (!Number.isInteger(maxDepth) || maxDepth < 1 || maxDepth > 32)
throw new RangeError("maxDepth must be an integer from 1 to 32.");
if (typeof includeStack !== "boolean")
throw new TypeError("includeStack must be a boolean.");
// Validate the root using the established translation contract.
translateError(error, options);
const seen = new Set(),
chain = [];
let current = error,
stopped = "complete";
while (current !== undefined) {
if (chain.length === maxDepth) {
stopped = "depth-limit";
break;
}
if (current !== null && typeof current === "object") {
if (seen.has(current)) {
stopped = "cycle";
break;
}
seen.add(current);
}
const object =
current !== null && typeof current === "object" && !Array.isArray(current)
? current
: undefined;
const raw = typeof current === "string" ? current : object?.message;
const message = typeof raw === "string" ? raw : String(current);
const name = typeof object?.name === "string" ? object.name : "Error";
const code = typeof object?.code === "string" ? object.code : undefined;
const translation = translateError(
{ name, message, ...(code ? { code } : {}) },
options,
);
chain.push({
depth: chain.length,
name,
message,
...(code ? { code } : {}),
translation,
...(includeStack && typeof object?.stack === "string"
? { stack: object.stack }
: {}),
});
current = object?.cause;
}
const result = { chain, stopped, truncated: stopped !== "complete" };
Object.defineProperty(result, "original", {
value: error,
enumerable: false,
});
return result;
}
export function renderDiagnosis(result) {
if (
!result ||
!Array.isArray(result.chain) ||
!result.chain.length ||
!["complete", "cycle", "depth-limit"].includes(result.stopped)
)
throw new TypeError("Provide a diagnosis result.");
return (
result.chain
.map(
(item, index) =>
`${index === 0 ? "Failure" : "Caused by"}: ${item.name}${item.code ? " [" + item.code + "]" : ""}: ${item.message}\n${renderTranslation(item.translation)}${item.stack ? "\n\nOriginal stack:\n" + item.stack : ""}`,
)
.join("\n\n---\n\n") +
(result.truncated ? `\n\nCause traversal stopped: ${result.stopped}.` : "")
);
}
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { definitions } from "./definitions.js";
export { diagnoseError, renderDiagnosis } from "./diagnostics.js";
export const listErrors = () => Object.keys(definitions);

/** Translate an ordered, bounded batch without mutating input. */
Expand Down
Loading
Loading