# @jchip/error reference

`@jchip/error` has helpers to print readable error stacks and an `AggregateError` subclass whose `stack` includes the stacks of the errors it holds. It has no runtime dependencies. ESM only (`"type": "module"`). Requires Node `^22.22.2 || ^24.15.0 || >=26.0.0`.

## Imports

```js
import { cleanErrorStack, aggregateStack, aggregateErrorStack, AggregateError } from "@jchip/error";
import * as jerr from "@jchip/error";
```

Everything is a named export from the package root. There is no default export and there are no subpaths (only `@jchip/error/package.json`).

Runtime exports: `cleanErrorStack`, `aggregateStack`, `aggregateErrorStack`, `AggregateError`.

There are no exported types besides the class `AggregateError`. The options type `CleanErrorStackOptions` is declared but not exported, so it cannot be imported by name. Use `Parameters<typeof cleanErrorStack>[1]` to get it.

## `cleanErrorStack(error, options?)`

```ts
type CleanErrorStackOptions = {
  replacePath?: false | string;
  ignorePathFilter?: (string | RegExp)[];
};

function cleanErrorStack(
  error: Error,
  options?: CleanErrorStackOptions
): string
```

Returns the text of `error.stack` with noisy stack frames removed and file paths shortened. The error is not modified.

| option | type | default | behavior |
| --- | --- | --- | --- |
| `replacePath` | `false \| string` | `` `${process.cwd()}/` `` (evaluated on each call) | Leading prefix removed from each kept frame path. Ignored when `false`, empty, or exactly 1 character long (so `"/"` does nothing). |
| `ignorePathFilter` | `(string \| RegExp)[]` | `[]` | Extra filters. A frame whose path matches any filter is dropped. Added to the built-in filter. |

Input selection:

- The text cleaned is `error.stack`, or `error.message` when the stack is falsy.
- When neither exists (or `error` is `null`/`undefined`), nothing is cleaned and the return value is `String(error && (error.stack || error.message))`. That gives `"null"` for `null` and `"undefined"` for `undefined`, `{}`, or an object with empty stack and no message.
- A non-string, truthy `stack` (such as a number) throws `TypeError` (`stack.split is not a function`).

Processing, applied to each line of the text split on `"\n"` or `"\r\n"`:

1. A line that does not match `/ {4,}at/` (an `at` preceded by 4 or more spaces anywhere in the line) is kept unchanged. This covers the message line, `Require stack:` lines, and frames indented with fewer than 4 spaces.
2. A frame line is parsed with `/( {4,}at )(?:([^(]*\()(.+)(\).*)|(.+))/`. It has two forms. `at name (location)` takes the location from the first `(` to the last `)`, so a `)` inside a path does not cut it short. `at location` has no parentheses. Node uses it for anonymous functions and ES module top-level code.
3. The frame is dropped when parsing fails (such as a bare `    at`), or unless the location contains `scheme://` (regex `/[^:]+:\/\//`) or `path.isAbsolute(location)` is true. That removes `node:internal/...` frames in either form, `(<anonymous>)`, `(index 0)`, and relative paths. `path.isAbsolute` is platform specific, so a Windows path like `C:\a\b.js` is dropped on POSIX.
4. Backslashes in the location are replaced with `/`.
5. The frame is dropped if the location matches the built-in filter or any `ignorePathFilter` entry. A `RegExp` entry uses `location.match(re)`. A string entry uses `location.includes(str)`. Falsy entries (such as `""`) are skipped.
6. Otherwise `replacePath` (when usable) is removed. A location that starts with `file://` + `replacePath` loses both, so an ES module URL under the current directory becomes a relative path. Any other location loses `replacePath` only when it starts with it (plain string, not a regex). A match in the middle of a path is left alone, and so is a `file://` URL outside `replacePath`.
7. The kept frame is rebuilt as `at` + name + new location + the text after the last `)`. Text before the 4-space indent is not kept.
8. Dropped frames are removed and the remaining lines are joined with `"\n"`. Blank lines in the original text are kept. A CRLF stack therefore comes back with LF line endings throughout.

Real stacks from CommonJS and ES modules are covered by tests on Node 22, 24 and 26 (`test/spec/real-stack.spec.ts`).

Built-in filter (always on, checked before `ignorePathFilter`):

```
/\/node_modules.*\/(pirates\/|isomorphic-loader\/lib\/extend-require)/
```

It drops frames from the `pirates` and `isomorphic-loader` require hooks. It matches on the backslash-normalized path.

Not removed: frames from other `node_modules` packages, and any absolute path outside `replacePath`. Only `node:` internals and other non-absolute locations go by rule 3.

```js
cleanErrorStack(err);
// Error: boom
//     at run (src/app.js:10:5)
//     at src/main.mjs:3:1
cleanErrorStack(err, { replacePath: false, ignorePathFilter: ["/node_modules/", /vitest/] });
```

## `aggregateStack(stack, errors)`

```ts
function aggregateStack(stack: string, errors: any[]): string
```

Builds a text from a top stack and a list of errors. Output is `stack`, then one entry per error, all joined with `"\n"`. Each error entry is its text with every line indented by 2 spaces (`replace(/^/gm, "  ")`, so blank lines become `"  "`).

The text of one error `e` is `e.stack`, else `e.message`, else `String(e)`. A falsy `e` gives `String(e)`. Examples: `1` gives `"1"`, `null` gives `"null"`, `{ message: "mm" }` gives `"mm"`.

Nested aggregate stacks indent again, so each nesting level adds 2 spaces.

Edge cases on `errors`:

- An array (or any object with a `map` function) is mapped. `[]` returns `stack` alone.
- A falsy `errors` (`null`, `undefined`, `""`) or a value with no `map` function (such as a string or a plain object without `map`) is treated as no errors, and also returns `stack` alone, for example `aggregateStack("t", null) === "t"`.
- A truthy `errors.map` that is not a function throws `TypeError`.
- `stack` is not checked. A non-string is joined as is.

## `aggregateErrorStack(error)`

```ts
function aggregateErrorStack(error: AggregateError): string
```

Returns `aggregateStack(top, error.errors)`. The parameter type is this package's `AggregateError` class.

- For an instance of this package's `AggregateError`, `top` is its saved original stack (`__stack`), which is the native stack text, including the header `AggregateError: <message>` and frames.
- For any other object, such as a native `globalThis.AggregateError`, `top` is `error.stack`. The native header and frames are kept.
- When that text is empty, `top` falls back to `error.message`, then `String(error)`. An instance of this class with an empty `__stack` never reads its own `stack`, since that getter calls this function.
- The list used is `error.errors`.
- The result is computed each time. It is cached only by the `stack` getter of this package's class.

```js
console.log(aggregateErrorStack(new AggregateError([new Error("error 1")], "test")));
// AggregateError: test
//     at ...
//   Error: error 1
//       at ...
```

## `AggregateError`

```ts
class AggregateError extends globalThis.AggregateError {
  readonly name: string;   // "AggregateError"
  errors: any[];
  stack: string;
  __stack: string;
  constructor(errors?: Iterable<any>, msg?: string, options?: { cause?: unknown });
}
```

An `AggregateError` whose `stack` also prints the errors it wraps. It is a subclass, not a polyfill. See "Polyfill behavior" below.

Constructor `new AggregateError(errors, msg?, options?)`:

- `errors` must be truthy and have a callable `[Symbol.iterator]`. Otherwise it throws `TypeError` with the message `input errors must be iterable but it's ${typeof errors}`. Falsy values give `undefined`, `object` (for `null`), `number`, and so on. A truthy number (`5`) gives `number`. A string is accepted since strings are iterable.
- `errors` is spread once with `Array.from`, and that list goes to both the parent constructor and `errors`. So a `Set` or a one-shot iterator such as a generator gives its elements, and a string `"ab"` gives `["a", "b"]`, as with the native class.
- `msg` is the message. When omitted the message is `""`.
- `options` is passed to the parent constructor, so `{ cause }` sets `cause` as the native class does.

Instance properties (all defined with `Object.defineProperty`, so `Object.keys(err)` is `[]`):

| property | attributes | value |
| --- | --- | --- |
| `name` | non-enumerable, non-writable, non-configurable own property | `"AggregateError"`. A subclass cannot override it by assignment. |
| `errors` | non-enumerable, writable, configurable | `Array.from(errors)`: a new array, even when `errors` is an array. |
| `__stack` | non-enumerable, writable, non-configurable | `this.stack` read right after `super()`: the native stack text of the error. Writing `stack` replaces it. |
| `stack` | non-enumerable, configurable accessor | Getter returning `aggregateErrorStack(this)`, computed on first read and cached. The setter replaces the header. |

`stack` details:

- The cache means `errors` changes made after the first read of `stack` are not reflected. Changes made before the first read are.
- Assigning `err.stack = text` replaces the header: `__stack` becomes `text` and the cache is cleared. The next read gives `text` followed by the errors' indented stacks, so the aggregated stacks are kept.
- `instanceof` works against the native class (`new AggregateError([]) instanceof globalThis.AggregateError` is `true`). A native instance is not an instance of this class.
- `console.log(err)` and `err.stack` in a debugger show the aggregate form. `cleanErrorStack(err)` therefore also cleans the nested error stacks, and the nested lines keep their 2-space indent.

## Polyfill behavior

- The package does not patch or define any global. Importing it has no side effects (`"sideEffects": false`).
- `AggregateError` here is a different class from `globalThis.AggregateError` (`AggregateError !== globalThis.AggregateError`). Its prototype parent is the global one.
- The module evaluates `class AggregateError extends globalThis.AggregateError` at import time. If the runtime lacks a global `AggregateError`, import throws. All supported Node versions (`engines`) have it, so there is no fallback implementation.
- Code that wants the aggregate-stack behavior must import this class explicitly. Native `AggregateError` instances are unchanged, and `aggregateErrorStack(native)` is the way to format them.
