caught-object-report-json
v14.0.0
Published
One JSON shape for every JavaScript error, built for logs and LLM harnesses.
Maintainers
Readme
Caught Object Report JSON
Uniform, bounded exception data for self-correction in agentic workflows, LLM harnesses, and agent graphs.
Normalize caught values and nested causes into one typed JSON format. Give your harness or LLM consistent exception data to inspect when correcting tool inputs or choosing a recovery step, instead of handling every SDK's error shape. Configure report size and traversal; select and redact fields before model exposure.
import { Corj } from 'caught-object-report-json';
try {
await fetchUser(id);
} catch (caught: unknown) {
logger.error({ error: Corj.makeReport(caught) });
}Table Of Contents
- Motivation
- Why CORJ for agentic development?
- Before Using This Library
- Installation
- Quick start
- LLM harnesses and agent graphs
- The report
- Options
- Redacting what the report emits
- Reporting without running the caught object
- Examples
- API
- Supported runtimes
- Report schema history
- Upgrading from v12
- Upgrading from v11
- Upgrading from v10
- Upgrading from v9
- Upgrading from v8
- Links
Motivation
- JavaScript has no standard JSON form for an
Error, andJSON.stringify(new Error('x'))gives{}. - Errors nest:
AggregateErrorhaserrors, any error may have acause, libraries add their own. What you catch can be a deep tree, and it can contain cycles. throwaccepts any value.throw null,throw 42andthrow undefinedare valid, and TypeScript types a caught value asunknown.- Property access on a caught object can itself throw (getters, proxies, revoked proxies).
CORJ represents these failures in a shared report format described by published JSON Schemas, including recovery from many property-access and serialization errors.
Why CORJ for agentic development?
- Uniform feedback for self-correction. Expose messages, captured values, and nested causes through one report contract. Correction code and LLM feedback can use consistent fields to inspect what failed, while SDK-specific payloads remain available for application-specific handling. Your harness selects a correction and validates the next action.
- Configurable diagnostic volume at reporting boundaries. Report a failure where a feature, tool, or graph node meets its
caller. A shared
CorjMakergives independently developed components one diagnostic contract. Configure depth, child count, and report size to limit diagnostic volume; compact reports omit predictable fields. The host selects relevance and redacts content before model exposure; byte limits do not measure tokens. - Types and fixtures for failure-path checks. Use
CorjReport,CorjReportNode, and typed options to check report consumers, then exercise the same boundary with deterministic failures. Test error capture, selected feedback, and recovery routing without calling a model; use live-model evals to assess the quality of proposed corrections. - Metadata for programmable diagnostics. Version and JSON Schema metadata identify the report format. Flat child reports,
paths, and
child_idslet application code inspect nested failures without parsing a different error shape for every SDK. Pass run IDs, tool names, or eval labels as the call'scontext, or keep them in your own envelope around the report.
CORJ supplies the diagnostic representation at your catch boundary, not a module system, LLM integration, or graph runner.
Your harness chooses what diagnostics to share and how to act on them. If a selected { code, reference } record or your
existing logger already meets the requirement, another serializer may add little value.
Before Using This Library
JSON.stringify(err, Object.getOwnPropertyNames(err)) is a fine quick fix when you do not need the edge cases: it does not
follow cause/errors into nested errors with different property sets, it throws on circular references and BigInt values, and
it produces a different shape for every kind of error.
A deliberate native Error/cause projection or an existing logger may already cover your needs. CORJ is useful when a
shared report schema, nested-cause representation and configurable size limits would otherwise require repeated custom code.
caught-object-report-json provides these reporting contracts:
- recovers from many property-access and serialization failures, and reports those secondary failures both in the report
itself (
reporting_errors) and throughonReportingError; - flattens nested errors into one array you can query with JSONPath-like tools, with cycles and shared errors reported once;
- keeps the whole report under a size limit while keeping it valid JSON with a known shape;
- omits what is predictable, so a plain
Errorcosts one stack array beside its id, fingerprint and version tag; - identifies one occurrence (
occurrence_id) and groups repeats of the same failure (fingerprint).
Getters, proxy traps and formatting hooks can execute during reporting. Catching thrown inspection errors and limiting output size do not isolate CPU or memory or interrupt a nonterminating hook. Raw reports can expose stack paths and enumerable secrets; they are not automatically appropriate for public responses or model context. Measure the final wrapped content with the target tokenizer when context fit matters.
Installation
Package: caught-object-report-json on npm.
npm install caught-object-report-jsonQuick start
import { Corj, CorjMaker } from 'caught-object-report-json';
// One-off, default options.
const report = Corj.makeReport(caught);
// Same report as a flat array: the root first, then every nested error.
const rows = Corj.makeReportArray(caught);
// Configure once, reuse everywhere.
const corj = new CorjMaker({ maxReportSize: 16_000, metadata: false });
const smaller = corj.makeReport(caught);
const derived = corj.withOptions({ maxDepth: 1 }); // a new maker, the original is unchangedFor a plain Error the default report is an id, a fingerprint, the stack and the version:
{
"occurrence_id": "CORJ_GM6YXYN2SS49JRGB3W1PM86D60",
"fingerprint": "fp1_cb088a0c78c9d945d29062a16465412a",
"stack": [
"Error: Something went wrong",
" at Object.<anonymous> (/home/user/work-dir/app.ts:2:9)",
" at Module._compile (node:internal/modules/cjs/loader:1120:14)"
],
"v": "corj/v0.15"
}Everything else a plain Error would report holds its expected value and is left out; see
Omitted expected values. Configuration and per-occurrence values share one input object:
const withContext = Corj.makeReport(caught, {
occurrenceId: 'req-42',
context: { runId: 'run-1', tool: 'search' },
});LLM harnesses and agent graphs
An LLM harness can catch failures from model clients and tools at the boundary of each agent graph node. CORJ converts the caught value into a report; the harness decides whether to retry, take a fallback edge, or end the run.
Uniform exception data for self-correction
Self-correction needs feedback about the previous attempt. One SDK throws an Error, another wraps the useful detail in
cause, and a tool may throw a plain object. CORJ gives your correction boundary a consistent outer format: message and
stack fields, string/JSON views, and links to nested causes. A shared reader can inspect that format across tools;
the contents of SDK-specific as_json payloads still need application-specific interpretation.
For example, a tool's validation failure may identify a missing input in its message or nested cause. After selecting and redacting that information, the harness can return it alongside the tool's input schema and attempted arguments. The LLM can propose corrected arguments; the host validates them before another call. Uniform reporting removes per-SDK outer-shape handling from this feedback boundary—it does not infer the right arguments or determine whether retrying is safe.
Keep the correction loop explicit:
- Capture the failure with
CorjMakerand associate it with the tool call and attempt in your own envelope. - Read the documented compact-field defaults or use
Corj.restoreExpectedValues; account for missing or truncated diagnostics. - Select and redact useful feedback. Treat error text as untrusted data, never as instructions or authority.
- Validate the proposed action, permissions, and retry safety; bound attempts and use a fallback when correction fails.
- Check the new result against the task's success criteria before declaring recovery.
CORJ supplies the exception data. Your harness supplies feedback selection, correction, execution, and outcome checks.
A typed failure boundary
The runnable example below wraps an ordinary node function, tests successful and failed model fixtures, and keeps failure
details in a typed result. No LLM API, credentials, or graph framework is needed. Save it as harness.ts in a project with
caught-object-report-json, TypeScript, and @types/node installed. With TypeScript 5, compile it using
npx tsc harness.ts --strict --skipLibCheck --types node --target ES2022 --module commonjs --outDir out, then run
node out/harness.js in a CommonJS project. With TypeScript 6 or newer, add --ignoreConfig when compiling this standalone file.
In this repository, run the same example
with npm run ts-file ./examples/example-12-agent-harness.ts after type checking; that runner does not check types.
import { strict as assert } from 'node:assert';
import { Corj, CorjMaker } from 'caught-object-report-json';
import type { CorjReport } from 'caught-object-report-json';
type Llm = { complete(prompt: string): Promise<string> };
type NodeResult<T> = { node: string } & (
| { ok: true; value: T }
| { ok: false; error: CorjReport }
);
const corj = new CorjMaker({
maxReportSize: 4096,
reportSizeUnit: 'utf8-bytes',
maxDepth: 2,
maxChildren: 4,
metadata: true,
});
async function runNode<T>(
node: string,
operation: () => Promise<T>,
): Promise<NodeResult<T>> {
try {
return { node, ok: true, value: await operation() };
} catch (caught: unknown) {
return { node, ok: false, error: corj.makeReport(caught) };
}
}
// Graph routing is application code, based on an explicit outcome.
function nextNode(result: NodeResult<string>): 'deliver' | 'fallback' {
return result.ok ? 'deliver' : 'fallback';
}
async function main() {
const workingModel: Llm = { complete: async () => 'A fixture answer.' };
const failure = Object.assign(new Error('Model request failed'), {
cause: { code: 'MODEL_UNAVAILABLE' },
});
failure.stack = 'Error: Model request failed'; // Stable diagnostic fixture.
const failingModel: Llm = {
complete: async () => {
throw failure;
},
};
const success = await runNode('answer', () => workingModel.complete('Hello'));
assert.equal(nextNode(success), 'deliver');
assert.deepEqual(success, {
node: 'answer',
ok: true,
value: 'A fixture answer.',
});
const failed = await runNode('answer', () => failingModel.complete('Hello'));
assert.equal(nextNode(failed), 'fallback');
if (failed.ok) throw new Error('Expected the failing fixture to fail');
const full = Corj.restoreExpectedValues(failed.error);
assert.equal(full.message, 'Model request failed');
assert.equal(full.children?.[0]?.path, '$.cause');
assert.deepEqual(full.children?.[0]?.as_json, { code: 'MODEL_UNAVAILABLE' });
assert.equal(failed.error.v, 'corj/v0.15');
assert.ok(failed.error.$schema?.endsWith('/corj/v0.15/report-object.json'));
assert.ok(Buffer.byteLength(JSON.stringify(failed.error), 'utf8') <= 4096);
const oversized = await runNode('answer', async () => {
throw 'x'.repeat(10000);
});
if (oversized.ok) throw new Error('Expected the oversized fixture to fail');
assert.equal(oversized.error.truncated, true);
assert.ok(Buffer.byteLength(JSON.stringify(oversized.error), 'utf8') <= 4096);
// Harness metadata lives outside CORJ's report and its size budget.
const evalRecord = { runId: 'eval-1', ...failed };
assert.equal(evalRecord.node, 'answer');
console.log('Success, fallback, nested-cause, and report-size evals passed.');
}
void main().catch((error) => {
console.error(error);
process.exitCode = 1;
});The model adapters are fixtures; the assertions test the harness boundary, not LLM output quality. In a real harness, wrap
your model or tool call with runNode and keep graph state, retries, and execution policy in the host. CORJ's onReportingError callback
reports problems encountered while building a report, not the original model or tool failure.
CORJ's flattened error graph describes cause and errors relationships, including shared errors and cycles. Its report
IDs and paths are not agent graph node IDs or execution traces. Keep those identifiers in the surrounding record, as above.
The metadata option only controls v and $schema; schemas describe report structure, not retryability or task success.
Consumers must account for omitted fields, truncated, and children_omitted rather than assume every diagnostic is present.
Before sending a report to an LLM or exposing it through an API, redact secrets and select appropriate fields. CORJ does not
redact, authorize disclosure, or make error text safe to treat as instructions. maxReportSize limits the report's compact
JSON bytes or code units, not model tokens, surrounding envelope fields, or processing time. Restoring omitted values or
pretty-printing also increases size. Apply your harness's context budget to the final content you actually send.
The report
Fields
Fields appear in this order. A field is present only when it carries information: a missing field means the expected value
listed in Omitted expected values, null means that producing the value failed.
| Field | Type | Meaning |
| --- | --- | --- |
| occurrence_id | string | Root only. Identifies this occurrence, see Occurrence id. On by default. |
| fingerprint | string | Root only. Equal for failures of the same kind from the same place, see Fingerprint. On by default. |
| id, path, level | string, string, number | Nodes of the flattened tree only (children, and every element of an array report). path is a JSONPath from the root such as $.cause.errors[0]; the root is $ at level 0. |
| truncated | true | Content or child reports were cut to fit maxReportSize. On the root it covers the whole report. |
| instanceof_error | boolean | caught instanceof Error. Omitted when true. |
| typeof | string | typeof caught. Omitted when "object". |
| constructor_name | string \| null | caught.constructor.name when it is a string. Omitted when it can be parsed from the first stack line. |
| message | string \| null | caught.message when it is a string. Omitted when it can be parsed from the first stack line. |
| as_string | string \| null | String(caught), or the result of the object's own toCorjAsString(). Omitted when it equals the first stack line. |
| as_json | JSON \| null | caught serialized with a circular-safe, BigInt-safe, length-limited serializer, without the children_sources properties; or the result of the object's own toCorjAsJson(). null when the value has no JSON form (undefined, functions, symbols) or serializing threw. Omitted when {}. |
| stack | string[] \| string \| null | caught.stack when it is a string, as an array of lines by default. |
| children_omitted | "max_depth" \| "max_children" \| "max_size" \| "not_inspected" \| "redacted" | This node has nested errors that were not reported, and what stopped them: a limit, the inspection mode, or the redact policy. |
| children | object[] | Root of an object report only. Every nested error, flattened breadth-first. |
| child_ids | string[] | Nodes of the flattened tree only. IDs of this node's direct children. |
| context | JSON \| null | Root only. The call's context rendered as JSON, see Context. Absent when the call passed none. |
| context_omitted | "max_size" | Root only. The context was dropped to fit maxReportSize. |
| reporting_errors | object[] | Root only. Failures met while this report was produced, at most 8, see Errors while reporting. Absent when there were none. |
| reporting_errors_omitted | "max_size" | Root only. The reporting errors were dropped to fit maxReportSize. |
| children_sources | string[] | Root only. The properties children were collected from. Omitted when ["cause", "errors"]. |
| as_string_format | "String" \| ".toCorjAsString" \| "derived" | How as_string was produced. "derived" means it was rebuilt from property descriptors, see inspection. Omitted when "String". |
| as_json_format | "safe-stable-stringify-with-length-limit" \| ".toCorjAsJson" | How as_json was produced. Omitted when the default. |
| v | "corj/v0.15" \| "corj/v0.15-full" | Root only. Report version, on by default (metadata option). |
| $schema | URL | Root only. JSON Schema of this report, off by default (metadata option). |
On the root the order is occurrence_id, fingerprint, then the fields above, then context and reporting_errors, then
children_sources and the metadata fields. An object report is the root fields plus children. An array report
(Corj.makeReportArray) is a list whose first element is the root, with id, path, level and child_ids like every other node;
the root-only fields — occurrence_id, fingerprint, context, reporting_errors, children_sources, v and $schema —
stay on that first element only.
Omitted expected values
Most caught objects are plain Error instances whose report fields hold the same values every time. Since corj/v0.11 those
fields are left out when they hold their expected value. A missing field means the expected value; null still means that
producing the value failed.
| Field | Omitted when it is |
| --- | --- |
| instanceof_error | true |
| typeof | "object" |
| as_json | {} (no enumerable own properties, which is what an Error gives) |
| as_string | equal to the first line of stack |
| constructor_name and message | together with as_string, when that line is exactly constructor_name + ": " + message (or just constructor_name for an empty message) |
| as_string_format | "String" |
| as_json_format | "safe-stable-stringify-with-length-limit" |
| children_sources | ["cause", "errors"] |
The rules apply to every node. constructor_name and message are parsed back out of the first stack line at the first ": ",
and are only omitted when that parse reproduces both values exactly: a subclass whose name still says Error, a multi-line
message, or a message changed after the stack was formatted keeps its fields. When a caught object has neither field but its
stack line would parse, as_string is kept as the signal that there is nothing to parse.
The expected values are exported as CORJ_EXPECTED_VALUES. Pass omitExpectedValues: false to keep every field, or fill them
back in on the consuming side:
import { Corj } from 'caught-object-report-json';
const full = Corj.restoreExpectedValues(Corj.makeReport(caught));
// full.instanceof_error === true, full.typeof === 'object', full.as_json is {},
// full.as_string is the first stack line, full.constructor_name and full.message are parsed from it,
// full.as_string_format, full.as_json_format and full.children_sources hold their defaults.Corj.restoreExpectedValues accepts object and array reports, returns a copy, and produces exactly what omitExpectedValues: false
would have produced. It does not add v or $schema, since a missing one means metadata was disabled.
Two report versions
| v | Produced by | Base fields |
| --- | --- | --- |
| corj/v0.15 | the default, omitExpectedValues: true | omitted when they hold the expected value |
| corj/v0.15-full | omitExpectedValues: false, or Corj.restoreExpectedValues | always present |
Each version has its own JSON Schema (see Links); $schema points to the matching one.
Stack as an array of lines
stack is stored as an array, one element per line, capped at 10,000 lines plus a [truncated] marker. Lines are easier to read in pretty-printed logs and to
query in log tools than one long string with escaped newlines. The split is literal: only \n separates, a \r before it stays
on its line, empty lines are kept, an empty stack becomes [""]. Only a string stack is reported; a stack getter that
throws yields null. Pass stackFormat: 'string' to keep the raw string.
Nested errors
Children are collected from the properties in childrenSources (cause and errors by default). An array property contributes
one child per element; holes and undefined values are skipped, null and primitives are reported. The tree is walked
breadth-first, so IDs grow with the level, and it is flattened: in an object report children holds every node and each node
lists its direct children in child_ids; in an array report the root is the first element and links the same way.
Every object is reported once. When the same error is reachable through two parents, or a cause chain loops back, the second
occurrence is a reference to the first node's id in child_ids, and a reference to the root uses the root's id ("root" by
default). See example 11.
Three limits keep the walk bounded, and each marks the node whose children it cut with children_omitted:
| Limit | Default | children_omitted |
| --- | --- | --- |
| maxDepth | 5 levels below the root | "max_depth" |
| maxChildren | 100 nodes per report | "max_children" |
| maxReportSize | 100000 UTF-8 bytes for the whole report | "max_size" |
Nodes that fit are kept in discovery order, so shallow errors win over deep ones.
Occurrence id
occurrence_id names one occurrence of one failure, so a report can be quoted back by id from a log line, a ticket or an
agent's next turn. It is a root field, on by default.
The value is resolved in this order, and the first valid one wins:
- the call's
occurrenceId:Corj.makeReport(caught, { ...options, occurrenceId: 'req-42' }); - each entry of
occurrenceIdSources, in order; - nothing, and the field is left out.
A valid id is 1 to 128 printable ASCII characters without spaces (/^[\x21-\x7e]{1,128}$/). A source that yields anything
else is passed over in silence: a missing id is not a reporting failure.
The call's own occurrenceId is checked the same way. It is per-occurrence runtime data — a request id off the wire — so a
value that is not a valid id does not throw: it is recorded as a reporting error (stage: 'other',
reportKey: 'occurrence_id', never quoting the value) and resolution continues with the sources. Only the shape of a call
input is your own configuration and still throws: something that is not an object, or an unknown key.
occurrenceIdSources takes four kinds of entry:
const maker = new CorjMaker({
occurrenceIdSources: [
{ sourceProperty: 'requestId' }, // one property of the caught object
{ path: ['response', 'headers', 'x-request-id'] }, // a path of 1 to 16 segments from it
({ caught }) => (caught as { id?: string }).id, // a function, given { index: -1, level: 0, path: '$', caught }
{ auto: 'random' }, // a generated id; this one always succeeds
],
});- The default is
[{ auto: 'random' }].nullor[]turns the field off. { sourceProperty }and{ path }read the root caught value only, through the same access the report uses, soredactskip rules apply andinspectiondecides whether a getter runs — at every segment of a path, not only the last. An entry may carry its owninspection;inspection: 'default'on an entry under a globalno-invokeruns caught code for that read. An element of an array is addressed as$.ids[0]whether you write the segment as0or as'0', so onepathsrule covers both spellings.- An excluded source immediately produces the literal
[redacted], regardless of the configured replacement, and stops the source chain. This marker loses uniqueness and correlation information. An explicit calloccurrenceIdstill wins. Numeric ordinary-object properties use dotted paths such as$.map.0; only array indices use brackets. - A function entry that throws is recorded as a reporting error (
stage: 'other',reportKey: 'occurrence_id') and resolution continues with the next entry. { auto: 'random' }yieldsCORJ_plus 26 Crockford base32 characters, fromglobalThis.crypto.getRandomValueswhen it is available andMath.randomotherwise. Because it always produces an id, an entry after it can never be reached and is rejected when the maker is built. The id is memoized per object, so one error object reported twice keeps one id; a thrown primitive gets a new one each time.- The id is never passed through the
redactpolicy: a scrubbed id would correlate with nothing. That is why it is a bounded ASCII token rather than free text, and why the bound is part of the size floor. - It is a correlation handle, not a secret. It authorizes nothing and proves nothing.
Fingerprint
fingerprint is equal for failures of the same kind from the same place, and different otherwise. An agent in a retry loop
compares two strings to learn "this is the same failure again"; a log tool groups by it. It is a root field, on by default,
hashed from a list of parts rather than from the report text.
const maker = new CorjMaker({ fingerprintParts: ['constructor_name', 'stack'] }); // the default
const sameFailureAgain = maker.makeFingerprint(caught) === previous;Every part contributes. fingerprintParts is an all-of list, unlike occurrenceIdSources, which is first-wins. The
order you write them in does not matter: named parts are sorted by label, and function parts keep their relative order after
them.
| Part | What it contributes for a node |
| --- | --- |
| 'constructor_name', 'message', 'as_string', 'typeof' | that report field of the node |
| 'stack' | the node's stack without its own header (see below) |
| { sourceProperty }, { path } | a value read from the node, with an optional per-entry inspection |
| a function | (context: { index, level, path, caught }) => unknown, called once per node |
{ path: ['a'] } is the one-segment case of { sourceProperty: 'a' }: the same read, the same label, the same hash — and listing
both is rejected as a repeated part.
as_json is deliberately not a part: it carries timestamps and per-occurrence ids, which would make every occurrence a new
kind of failure. Name a stable property with { sourceProperty } instead. A whole-report hash was rejected for the same reason, and
because every format bump would then change every fingerprint.
Parts apply to every node, root and children, in discovery order, and each node's values are hashed together with its
path, so the same error nested one level deeper fingerprints differently.
The 'stack' part excludes the node's own header, so 'message' and 'stack' do not hash the same text twice: when the
stack starts with the node's as_string and that prefix ends the line (or the whole stack), that prefix is cut;
otherwise, when the stack has a V8 frame line (\n at ), everything before the first one is cut; otherwise the stack is
used whole, which is what Firefox and Safari stacks need. The line-boundary condition matters for a short
.toCorjAsString(): 'E' is a prefix of Error: <message>, and cutting there would leave the message in the hash.
Values are taken after redaction and before omission and size limiting:
| Moves the fingerprint | Does not move it |
| --- | --- |
| maxDepth, maxChildren, childrenSources | maxReportSize, reportSizeUnit, maxContextSize |
| the redact policy | stackFormat |
| inspection | omitExpectedValues and the rest of the omission pass |
| a new build, because stacks carry line numbers | the report format version, and metadata |
A nested value — an object or an array — goes through a key-sorted JSON view with a fixed 16,384-byte cap, so property
assignment order and the report budget cannot move the hash. Every string value is likewise cut to 16,384 UTF-16 code
units, after redaction, so the hash input stays bounded whatever the caught object carries: two strings that differ only
past the cap share a fingerprint by design. A bigint contributes <digits>n and a non-finite number its String() form
(NaN, Infinity, -Infinity); a function, a symbol, a missing property or a value the policy dropped contributes
nothing.
A root without a string stack (a thrown primitive or a plain object), or whose part values are all empty, also hashes its
typeof and as_string, so thrown primitives do not all share one fingerprint: a thrown string has
constructor_name: "String", and an emptiness rule alone would not catch it.
The output is fp1_ plus the first 32 hex characters of a SHA-256 digest, computed by a bundled pure-JavaScript
implementation because the web platform has no synchronous digest and this package has no dependencies. The fp1 prefix
names the recipe: if it ever changes, fingerprints from the two versions are not comparable, and the prefix is how a reader
can tell.
Computed fingerprints are omitted with a fingerprint reporting error when new input safety handling, lossy snapshot
preparation, colliding redacted keys, or pattern changes whose transform skips a legacy scrub pass prevent the supported
canonical input from being reconstructed. Supported fp1_ hashes retain their recipe, including its existing 16,384-byte nested JSON and 16,384-code-unit string output cuts. A limit
in an independent JSON view does not invalidate an otherwise complete fingerprint. Explicit call fingerprints still win.
Function parts use the node path; CORJ does not infer a source path from application code. Numeric and string path segments
read the same property and policy path but remain distinct labels in the existing fingerprint recipe.
maker.makeFingerprint(caught) computes the value alone — discovery and node fields, without as_json, the context or the
limiter. It returns undefined when fingerprintParts is null or []. makeFingerprint(caught, { requireStack: true })
returns a value only when the hash is backed by the root's own stack frames: 'stack' is one of the parts, and the
root's stack, as the part hashes it (after redaction, after the header is cut), still carries at least one frame line. A
frame line is one that names a place — at fn (/app/a.js:1:1), at async Promise.all (index 0), fn@/app/a.js:1:1 — so
prose that merely says "at", or an address with a port, is not one.
Everything else is undefined — a thrown primitive, a plain object, an error with no stack, a stack that carries no frames
because a caller assigned a sentence to it, a stack a redact skip rule replaced, a stack inspection: 'no-invoke'
withheld, a recipe that does not name 'stack', and an error the runtime created without frames, such as a Node fs
callback error (ENOENT) or the AggregateError from Promise.any: their stack is the header line and nothing else, so
there is no place to hash and undefined is the rule working, not the option failing. A stack a skip rule replaced or inspection withheld fails the rule
because the read was skipped, not because of how the replacement reads: a policy is free to choose frame-shaped text. The
option is the caller's alone — it never moves the fingerprint a report carries, and makeFingerprint(caught) without it
is unaffected. A call argument outranks the parts:
Corj.makeReport(caught, { ...options, fingerprint: 'checkout-timeout' }), 1 to 64 printable ASCII characters without spaces. Like
occurrenceId, a call fingerprint that is not such a token is recorded (stage: 'other', reportKey: 'fingerprint') rather
than thrown, and the parts are hashed instead.
Hashing cannot fail a report: a failure while computing the fingerprint is recorded the same way and the report simply has
no fingerprint field.
Showing a fingerprint to an untrusted audience. Anyone who can guess the hashed values can compute the hash and confirm the guess, so what the hash input contains is what the published fingerprint discloses. A value with no stack is hashed from its own text, and that text is often guessable: a reviewer recovered a
"PIN 4921 rejected for [email protected]"message from such a fingerprint in ten milliseconds. Keep'stack'in the recipe when the fingerprint is published, and compute it withmaker.makeFingerprint(caught, { requireStack: true }), which returns nothing at all rather than a hash a reader could confirm.What that leaves is a hash of real stack frames — and of every other part in the recipe: add
'message'and the message is in the hash input beside the frames, as confirmable as it ever was. Frames themselves are unguessable only to a reader who does not know your deployed source and paths; a reader who has the bundle can enumerate call sites. The option removes the case where the text is the hash; it does not make a fingerprint a secret.
Context
A call may carry data of the caller's own beside the caught object — a request id, a run id, the tool that failed:
const report = Corj.makeReport(caught, {
context: { runId: 'run-1', tool: 'search', attempt: 2 },
});
// report.context: { runId: 'run-1', tool: 'search', attempt: 2 }contextis a root field, rendered with the same bounded, cycle-safe, redacted JSON view the report uses foras_json, in a document of its own rooted at$context. Aredactpathsrule written for the caught value does not match inside it, and one written for it does not match inside the caught value;keys,patternsandtransformsee both.maxContextSizecaps it on its own (16384by default,nullfor no separate cap), insidemaxReportSize.- It is counted in
maxReportSize, and it is the first thing dropped when the report does not fit; see Size limit. - It is a call argument rather than a maker option, so one shared maker still serves every call.
A JSON view of any value
The pipeline behind as_json is available on its own, under the maker's inspection and redact options:
const maker = new CorjMaker({ redact: { keys: ['password'] } });
const view = maker.makeJsonView(payload, { root: '$request', maxSize: 4096 });
// { value: <JSON or null>, truncated: boolean, errors: CorjReportingError[] }
const line = maker.scrubText(`login failed for ${user}`, { path: '$audit' });makeJsonView(value, { root?, maxSize? })returns{ value, truncated, errors }.valueisnullwhen the value has no JSON form or producing it threw;errorsholds up to 8 failures met on the way, and never touches a report's own list.rootis'$'(the default) or a named document root matching/^\$[A-Za-z_][A-Za-z0-9_]*$/, such as'$context'or'$request'. A property path always continues with.or[, so a named root can never collide with a path into the caught value. Anything else throws aTypeError.maxSizedefaults to the maker'smaxReportSize; it is a safe integer of at least 256, ornullfor no bound. The floor is below the report's 512 because a single value has no minimal report to fit, only itself.- Unlike a report node's
as_json, a view does not hide thechildrenSourcesproperties: a view has no children. scrubText(text, { path?, reportKey?, sourceProperty? })applies the policy's scrub rules to one string CORJ never sees, withstage: 'warning'. Without a policy it returns the text unchanged.
Size limit
The entire report is limited to 100,000 UTF-8 bytes of compact JSON by default. All fields, metadata, escaping,
punctuation and children share that budget; for Corj.makeReportArray the limit applies to the complete array.
const corj = new CorjMaker({ maxReportSize: 64_000, reportSizeUnit: 'utf8-bytes' });maxReportSize must be a safe integer of at least 512, or null to disable the limit. reportSizeUnit is
'utf8-bytes' (Buffer.byteLength(json, 'utf8')) or 'utf16-code-units' (json.length). Invalid values throw when the
maker is built. The floor is 512 because that is the smallest budget the worst case of a minimal report is guaranteed to
fit: a 128-character occurrence_id, a 64-character fingerprint, both omission flags, and the array form.
Input safety ceilings apply even with maxReportSize: null or a view's maxSize: null: 10,000 JSON candidate entries per
serialization, depth 128, 10,000 child-source candidates across graph discovery, 16,384 decimal BigInt digits before decimal
conversion, 1,000,000 UTF-16 units per text subject/intermediate and in aggregate per serialization or prepared snapshot,
10,000 display stack lines, and 128 prototype lookup steps. Dropped values and repeated children still consume candidate budgets. Oversized
text is withheld whole before patterns or transforms, since cutting it first could split a secret. Input exhaustion sets
truncated and retains safe partial output with existing markers. Display stack truncation may still fingerprint the full
supported raw stack.
These bound library-controlled work, not elapsed time. Native enumeration/allocation, proxy traps, default-mode hooks and getters, individual regular expressions (including overridden methods), and application callbacks cannot be interrupted synchronously. A replacement may allocate before CORJ can check its result.
Reports that fit are preserved exactly. Oversized reports set truncated: true on the root. Optional root parts go first,
each whole and each leaving a flag behind, and only then is error content reduced:
contextis dropped whole, however small it is, andcontext_omitted: "max_size"takes its place.reporting_errorsis dropped next, andreporting_errors_omitted: "max_size"takes its place. Without that flag a reader of a trimmed report would conclude that inspection had been clean.- Only then is error content trimmed, as below.
occurrence_id, fingerprint and v are never trimmed. They are counted into the budget and survive into the minimal
report, so even a report cut to the floor can still be quoted back by id and grouped by fingerprint.
What trimming content keeps valid:
- Strings retain a prefix followed by
[truncated](CORJ_TRUNCATED_MARKER). - Arrays inside
as_jsonretain leading elements and append the marker; objects retain leading properties and may add"...": "[truncated]". - Circular references inside
as_jsonbecome[circular](CORJ_CIRCULAR_MARKER). - All content fields share the budget. The limiter reserves a small amount per field, retains a prefix of complete child reports, and spreads the remaining room across fields. Optional metadata is dropped before content is reduced further.
- Dropped children get
children_omitted: "max_size"on their parent, and their IDs disappear fromchild_ids. - If even a minimal report cannot fit (custom IDs alone can exceed the budget), the result is a root-only report with
as_string: "[truncated]"andas_json: null, keepingoccurrence_id,fingerprint,vand any omission flags.
Pretty-printing and fields added later by a logger increase the final size; reserve room for those. Child discovery and custom formatters run before the budget is allocated, so input size still affects processing time. See example 10.
Errors while reporting
Nothing the caught object does can make Corj.makeReport throw. When a getter, proxy trap, toString, toCorjAsJson or the
serializer throws, the affected field becomes null (or the affected child is skipped) and CORJ records what happened:
type CorjContext = {
stage:
| 'prop-access'
| 'as_string'
| 'as_json'
| 'children'
| 'limit'
| 'redact'
| 'warning'
| 'other';
path: string; // JSONPath of the value; '$' is the caught root, '$context' the call's context
reportKey?: CorjReportKey; // report field being produced, when known
sourceProperty?: string; // property of the caught object being read, when known
};
type CorjReportingError = CorjContext & { error: string };
type CorjErrorHandler = (reportingFailure: unknown, record: CorjReportingError) => void;Every record goes to two places:
- the root field
reporting_errors, so the report explains its own gaps: a reader who seesmessage: nullfinds the reason in the same JSON, without access to the process's stderr. At most 8 rows are kept; later failures still reach the handler. onReportingError(reportingFailure, record), which gets the raw value thrown while reporting, for sinks such as Sentry. The handler receives a shallow copy of the record, so a handler that rewrites its argument cannot rewrite the report's row. The default handler prints oneconsole.warnline per failure; passonReportingError: () => {}to silence it, or your own handler to route failures into your logs.
Records are collected per call, not per maker, so a getter that re-enters the same shared maker does not mix its failures into the outer report.
record.error is the thrown value as text, scrubbed by the redact policy before it is cut to 256 characters, so a
secret straddling the cut cannot survive the cut. path and sourceProperty go through the policy as well. A stage: 'redact'
record — the policy itself threw — carries the policy's replacement as its error instead of the thrown message, which
could quote the very content the policy was protecting; its path may be the replacement too, because a policy that has
just failed is not consulted again.
Failures in the finishing pass — stage: 'limit', and stage: 'other' from the omission pass — happen after the report
body is assembled and the budget is spent, so they reach the handler only and never appear in reporting_errors.
Only your own configuration throws: unknown option names, invalid option values and a call input of the wrong shape — not an
object, or an unknown key — raise TypeError or RangeError from the CorjMaker constructor, Corj.makeReport, Corj.makeReportArray
and the maker's own methods. The call's occurrenceId and fingerprint are runtime data rather than configuration: a value
that fails its token pattern is recorded as a reporting error and passed over, so reporting one error never throws a second
one inside the catch block that was reporting the first.
Options
Every option is optional; missing ones keep their defaults. CORJ_DEFAULT_OPTIONS exports the defaults.
| Option | Default | Meaning |
| --- | --- | --- |
| maxReportSize | 100000 | Size limit of the compact JSON of the whole report. null disables it. |
| reportSizeUnit | 'utf8-bytes' | Unit of maxReportSize; also 'utf16-code-units'. |
| maxContextSize | 16384 | Own size cap of the call's context, inside maxReportSize. null leaves only the report budget. |
| omitExpectedValues | true | Leave out fields holding their expected value. |
| stackFormat | 'lines' | 'lines' stores at most 10,000 stack lines plus a truncation marker, 'string' the raw string. |
| inspection | 'default' | How much of the caught object may run while it is reported. 'no-invoke' reads property descriptors only, see Reporting without running the caught object. |
| redact | null | Field selection and redaction applied to everything the report emits, see Redacting what the report emits. |
| metadata | { v: true, $schema: false } | Which of v and $schema to add to the root. true adds both, false neither, an object sets them individually. |
| maxDepth | 5 | Deepest level of nested errors to report. 1 reports caught.cause but not caught.cause.cause. |
| maxChildren | 100 | Most child reports in one report. |
| childrenSources | ['cause', 'errors'] | Properties to collect children from. They are also left out of as_json. |
| occurrenceIdSources | [{ auto: 'random' }] | Where occurrence_id comes from; an ordered list, first valid id wins. null or [] omits the field, see Occurrence id. |
| fingerprintParts | ['constructor_name', 'stack'] | What fingerprint is hashed from; every part contributes. null or [] omits the field, see Fingerprint. |
| makeReportId | 'root' / discovery index | (context: { index, level, path, caught }) => string, called once per node; index is -1 for the root. |
| onReportingError | console.warn | (reportingFailure, record) => void, called for every failure, including the ones reporting_errors could not keep, see Errors while reporting. |
A caught object can take over its own representation by implementing toCorjAsString(): string and/or
toCorjAsJson(): unknown. Both are called with this bound to the object and one argument { path, options }. A method that
throws is reported through onReportingError and the default format is used; a method that returns an unusable value (a non-string, or a
value without a JSON form) falls back silently. The formats used are recorded in as_string_format and as_json_format.
Redacting what the report emits
A report can carry credentials: in a property, a message, a stack, as_string, a nested cause, or the text of an inspection
failure. CORJ supplies the mechanism; which content is sensitive stays your decision.
A policy has two kinds of rule. Skip rules (keys, paths) name properties CORJ never reads, so an excluded getter never
runs. Scrub rules (patterns, transform) rewrite text wherever the report emits it.
const maker = new CorjMaker({
redact: {
keys: ['password', /^authorization$/i],
paths: ['$.cause.config.headers'],
patterns: [/sk-live-[A-Za-z0-9]+/g],
replacement: '[redacted]',
transform: (value, { sourceProperty }) => (sourceProperty === 'url' ? '[url]' : value),
},
});| Rule | Kind | Meaning |
| --- | --- | --- |
| keys | skip | Property names never read, wherever they appear. A string matches exactly and is case-sensitive; use a RegExp for anything else. |
| paths | skip | JSONPaths never read, e.g. '$.cause.config.headers'. A string matches exactly. |
| patterns | scrub | RegExps replaced in every string the report emits. Each must carry the g flag, or the policy is rejected. |
| transform | scrub | (value, context) => unknown, run last on every emitted value. Returning undefined leaves the field out. |
| replacement | — | What skipped and scrubbed content becomes, inserted literally ($& and $1 are not expanded). Defaults to CORJ_REDACTED_MARKER ('[redacted]'). |
To remove a secret's text, use patterns. Skip rules select properties, not content, and error text is duplicated
across message, stack and as_string, so skipping one read does not remove the text from the others:
Corj.makeReport(new Error('boom SECRET'), { redact: { keys: ['message'] } });
// message: '[redacted]' stack[0]: 'Error: boom SECRET'The caught object's own toString, and V8's stack formatting, read message themselves; no policy can stop code CORJ did not
call. Add inspection: 'no-invoke' if the getter must also never run.
The transform context is { stage, path, reportKey, sourceProperty }, where stage is 'prop-access', 'as_string', 'as_json' or
'warning'. ('children' reaches only the skip rules, never transform: a children source is excluded before it is read or
not at all.)
Transforms receive safe snapshots by default. An object or array is copied into detached plain data before the
callback runs. CORJ applies configured key/path exclusions and string patterns to nested keys and values first. Objects have
no prototype; arrays retain their array identity with inherited executable behavior removed. Only own enumerable data is
copied. Accessors, functions, symbols and BigInts become [not-inspected]; ancestor cycles become [circular]. Preparation
never calls getters or serialization hooks. The source object is unchanged. This protects configured secrets; it cannot
identify secrets absent from your policy. Callback context paths and source-property names deliberately retain original
source names for policy matching; treat that metadata as sensitive.
let callbackPassword: unknown;
const maker = new CorjMaker({
redact: {
keys: ['password'],
transform: (value, { path }) => {
if (path === '$' && typeof value === 'object' && value !== null) {
callbackPassword = (value as Record<string, unknown>).password;
}
return value;
},
},
});
const report = maker.makeReport({ user: 'ada', password: 'hunter2' });
// callbackPassword === '[redacted]'
// report.as_json: { user: 'ada', password: '[redacted]' }Returned containers are sanitized before later callbacks or output. If scrubbed object keys collide, CORJ preserves members
with suffixes such as name (2) and name (3), skipping names already emitted. Policy paths continue to use original keys.
Snapshot inspection failures fail closed. Other application callbacks, including fingerprint/source functions,
makeReportId and a custom onReportingError, keep receiving their documented original inputs and require your trust.
What the policy reaches
- every report field:
message,stack,constructor_name,as_string, and every value and property name insideas_json, at any depth; id, only when you supplymakeReportId, since such an id may be built from the caught object. Default ids are structural and never rewritten, sochild_idskeeps linking children to their reports. Two custom ids that scrub to the same text collide;- the output of a caught object's own
toCorjAsString()andtoCorjAsJson(); - every child report, not only the root;
- the call's
context, as a document of its own rooted at$context, and every value amakeJsonViewview produces under its own named root; - every value a
fingerprintPartsentry reads, whatever its type: a number or a boolean is as identifying as a string, and the fingerprint is meant to be published. Each value is offered under its own path —$.detailsfor{ sourceProperty: 'details' },$.details.tokenfor what is inside it,$.cause.detailson a child — so apathsrule or a path-keyedtransformthat rewrites it inas_jsonrewrites the same value in the hash input; - the line the default
onReportingErrorprints about a failure, which is otherwise built from the caught object's own text. A customonReportingErrorreceives the caught object unchanged and must apply its own policy.
A children source the policy excludes is not followed: the node gets children_omitted: "redacted" instead of a child report.
Report schema, cycle handling and the maxReportSize budget are unaffected — redaction runs before the size limit is applied.
Property names that scrub to the same text retain their values with suffixes such as (2) and (3) in as_json.
Because keys matches a property name wherever it appears, a broad name has a broad reach: keys: ['name'] also blanks
constructor_name, which is read as constructor.name.
Since corj/v0.14, paths rules also see $context... and any named root a makeJsonView view uses, so an unanchored RegExp
such as /\.headers$/ starts matching there too. It fails safe — more is redacted, not less — but to keep a rule on the
caught value alone, anchor it with ^\$\.:
const onCaughtOnly = { paths: [/^\$\..*\.headers$/] };
const onContextOnly = { paths: ['$context.user.email'] };The final occurrence_id, fingerprint and default report id are not post-processed by transforms or string patterns.
An excluded occurrence-ID source still yields the literal [redacted] and stops source resolution, as documented above; an
explicit call occurrenceId wins. Computed fingerprints hash policy-processed values, and preserving default structural IDs
keeps child_ids linked to their reports.
Applying the policy to your own text
A custom onReportingError receives the caught object unchanged, and anything you log next to a report is text CORJ never sees.
maker.scrubText applies the maker's own policy to one such string: it always returns a string, and without a policy it
returns the text unchanged.
import { CorjMaker } from 'caught-object-report-json';
const maker: CorjMaker = new CorjMaker({
redact: { patterns: [/sk-live-[A-Za-z0-9]+/g] },
onReportingError: (caught, { stage, path }) => {
const line = `corj ${stage} failed at ${path}: ${String(caught)}`;
// `stage: 'warning'` is the redaction stage for text a handler prints.
logger.warn(maker.scrubText(line, { path }));
},
});scrubText(text, { path?, reportKey?, sourceProperty? }) runs patterns and then transform with stage: 'warning'. A transform that
drops the value, returns a non-string or throws yields the replacement. For a value that is not a string, use
maker.makeJsonView, which applies the whole policy including the skip rules.
Corj.resolveRedactPolicy(input) validates a policy once, ahead of any maker, so a bad policy fails at startup rather than
inside a catch block. It returns null for null and undefined, and accepts an already-resolved policy.
When the policy itself fails
A throwing matcher or transform fails closed: the value it was asked about becomes the replacement rather than passing
through. The failure is reported through onReportingError with stage: 'redact', once per value, so a transform that always throws
reports several. Reporting a failure does not consult the same policy again, so such a policy cannot recurse.
What a policy cannot do
- It cannot discover a secret it was not told about.
keys,pathsandpatternsare your configuration, not detection. - It does not make error text trustworthy. A redacted message is still text a caught object controlled: it is diagnostic material to read, never instructions to follow.
- Disclosure policy stays application-owned. This package supplies the traversal; it ships no default list of sensitive names.
Reporting without running the caught object
By default, describing a caught object runs some of its code: reading message calls a getter if one is defined, as_string
calls toString, as_json calls toJSON, and both toCorjAsString and toCorjAsJson are used when present. maxReportSize
bounds how much output that produces; it cannot interrupt a synchronous hook that never returns.
inspection: 'no-invoke' reads values off property descriptors and calls none of those hooks.
let inspected = 0;
const caught = {
get message() {
inspected++;
return 'failure';
},
plain: 'kept',
};
Corj.makeReport(caught, { inspection: 'no-invoke' });
console.log(inspected); // 0produces
{
"occurrence_id": "CORJ_JM2Q74GWR4K1WHV6TCB51PG17V",
"fingerprint": "fp1_289484f2fab1bd06d4b560b3f0ad51b7",
"instanceof_error": false,
"constructor_name": "Object",
"message": "[not-inspected]",
"as_string": "[object Object]",
"as_json": { "message": "[not-inspected]", "plain": "kept" },
"as_string_format": "derived",
"v": "corj/v0.15"
}What the mode does
| | 'default' | 'no-invoke' |
| --- | --- | --- |
| a getter on message, stack, constructor, cause, errors | invoked | replaced with CORJ_OMITTED_MARKER |
| stack of a host whose name or message is a getter | invoked | replaced with CORJ_OMITTED_MARKER, without the stack descriptor being read |
| a getter reached while building as_json | invoked | replaced with CORJ_OMITTED_MARKER |
| toJSON, toCorjAsJson | used | never called |
| toString, toCorjAsString | used | never called; as_string is derived instead |
| a children source behind a getter | followed | left unread, the node gets children_omitted: "not_inspected" |
inspection and redact compose: redact decides what may be reported, inspection decides how much may run to report it.
CORJ_OMITTED_MARKER ("[not-inspected]") marks content that exists but was not read. It is distinct from an absent field,
which means the caught object never had that property, and from null, which still means that producing the value threw.
as_string is rebuilt rather than obtained, and as_string_format records this as "derived":
- a caught object whose
toStringisError.prototype.toString— every ordinary error — is rendered asname: messagefrom values read off descriptors. This matches the built-in whenevernameandmessageare strings or absent; the built-in coerces other types (name: 404gives404: …) and this mode treats them as absent instead; - a caught object whose
toStringisObject.prototype.toStringis rendered as[object Tag], where the tag comes from aSymbol.toStringTagdata property or falls back toObject,ArrayorFunction. The built-in is not called, because it performs a[[Get]]that aProxywould turn into agettrap; - any other
toStringbelongs to the caught object, soas_stringbecomes the marker; - primitives are stringified normally.
Native errors keep their stack on V8 (Node, Chromium, Bun), which exposes stack as an own accessor property; no-invoke
calls that one engine-provided getter, identified by reference, and no other accessor. Two cases withhold it instead:
- V8 formats the stack string on first read, and formatting reads
nameandmessage. When either is an accessor,stackis reported as the marker on every engine, whatever shape that engine gives it: on Node 18 and 20 the lazily formatted stack is an own data property, andObject.getOwnPropertyDescriptor(caught, 'stack')is itself that first read, so the descriptors ofnameandmessage- which are safe to read - settle the question before anystackdescriptor is looked at. The same rule withholds a plainstackstring on a host whosenameormessageis an accessor, inas_jsontoo: telling the two shapes apart would mean performing the read the mode is avoiding. - The getter is identified by reference, so an error from another realm (a
vmcontext, an iframe) is not recognized and itsstackandas_stringare both the marker. Wherestackis a data property andnameandmessageare data properties too, its value is read off the descriptor like any other property's.
Error.prepareStackTrace is a global application hook rather than anything the caught object owns; if your process installs
one it still runs during formatting, and no in-process option can prevent that.
Cause chains, errors arrays, cycle handling and the report size limit behave as they do by default.
What the mode is not
It is not a sandbox, and maxReportSize is not a timeout.
- Reading a descriptor off a
Proxyruns itsgetOwnPropertyDescriptortrap, walking the prototype chain runs itsgetPrototypeOftrap, and listing properties foras_jsonruns itsownKeystrap. A trap that never returns still hangs the caller, and a trap can fabricate the descriptors it returns. - For hard CPU isolation, produce the report behind a worker or process boundary. No synchronous option can provide it.
- The mode reduces fidelity on purpose. A
Date,RegExp,URLor typed array thrown directly carries its owntoString, so itsas_stringis the marker; aMap,SetorPromiseinheritsObject.prototype.toStringand aSymbol.toStringTagdata property, so it reads[object Map]. Lazily computed properties are reported as the marker rather than as their values.
Examples
1. Syntax error
(Run with npm run ts-file ./examples/example-1-syntax-error.ts)
try {
JSON.parse(undefined);
} catch (caught: unknown) {
caught.heh = 123n;
caught.heh_1 = new Number(123);
const report = Corj.makeReport(caught);
console.log(JSON.stringify(report, null, 2));
}prints
{
"occurrence_id": "CORJ_…",
"fingerprint": "fp1_…",
"as_json": {
"heh": 123,
"heh_1": {}
},
"stack": [
"SyntaxError: \"undefined\" is not valid JSON",
" at JSON.parse (<anonymous>)",
" at Object.<anonymous> (/home/user/work-dir/caught-object-report-json/examples/example-1-syntax-error.ts:6:8)",
" at Module._compile (node:internal/modules/cjs/loader:1929:14)",
" at Module.m._compile (/home/user/work-dir/caught-object-report-json/node_modules/ts-node/src/index.ts:1618:23)",
" at node:internal/modules/cjs/loader:2060:10",
" at Object.require.extensions.<computed> [as .ts] (/home/user/work-dir/caught-object-report-json/node_modules/ts-node/src/index.ts:1621:12)",
" at Module.load (node:internal/modules/cjs/loader:1651:32)",
" at Module._load (node:internal/modules/cjs/loader:1443:12)",
" at wrapModuleLoad (node:internal/modules/cjs/loader:261:19)",
" at Module.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:154:5)"
],
"v": "corj/v0.15"
}2. Axios error
AxiosError#toJSON does not include response headers and response
data (issue), so you'll have to add it yourself.
(Run with npm run ts-file ./examples/example-2-axios-error.ts)
const axiosClient = axios.create