npm package: @lvigil/err
Description: Enhanced Error with message history, context dict, and flag dict — flat, not chained, designed for logging.
Author: Ben P.P. Tung
Latest version: 1.3.0

# err

A minimal, environment-agnostic enhancement to JavaScript's native `Error`.

`err` provides a simple way to attach **runtime context**,
**human-controlled message breadcrumbs**, and **error flags for program logic**
(like `err.code`), while preserving the behavior, simplicity,
and semantics of a standard `Error`.

It does **not** modify prototypes.
It does **not** wrap stack traces.
It remains fully compatible with all runtimes (Node, browser, Deno, Bun, Workers).

Designed for **logging** and **debugging** — when you read the log,
you see everything in one place, not scattered across nested layers.

## Installation

```sh
pnpm add @lvigil/err
```

---

## The Three Core Dimensions

This library handles three things:

| Dimension | What it is | How to use | Behavior |
|-----------|-----------|------------|----------|
| **message_history** | How the error bubbled up | Add with `.m()` | append |
| **context_dict** | State at each layer for debugging | Pass as 2nd parameter | merge, old wins |
| **flag_dict** | Flags for program logic | Pass as 3rd parameter | merge, new wins |

**`context_dict`** — for debugging. Any context you need, as key-value pairs.

**`flag_dict`** — for coding. Only use it when the caller needs `if (err.code === ...)` checks. Don't write it just for the sake of writing.

### Flat, not chained

ES2022 `cause` creates a **linked chain** — each layer wraps the previous error. To see the full picture, you need to recursively walk through `err.cause.cause.cause...`. Context is scattered.

This library takes a **flat** approach — all three dimensions accumulate into single, accessible structures:

- `msgs[]` — one array, read top to bottom
- `original{}` — one object, all context merged
- `flag_dict` — directly on `err`, latest values ready for checks

**Trade-off:** flat means possible key conflicts when merging. Each dimension handles this differently:

- **message_history** — no conflict, just **append**
- **context_dict** — **old wins**, because the context closer to the error source is more valuable for debugging
- **flag_dict** — **new wins**, because the caller may need to update flags for branching logic

When you `console.log(err)`, everything is right there. No recursion needed.

---

## Quick Start

### Create an error

```js
import { Err } from '@lvigil/err'

throw Err('Invalid config', { config })
```

### Wrap and rethrow

One line — context, message, done:

```js
import { OnErr } from '@lvigil/err'

catch (e) {
  throw OnErr(e, { userId, file }).m('Failed to load user data')
}
```

---

## Usage

### Creating an error

```js
import { Err } from '@lvigil/err'

throw Err('invalid payload format', { payload })
```

Result:

```js
{
  message: 'invalid payload format',
  msgs: ['invalid payload format'],
  original: { payload },
  stack: '...'
}
```

### Enhancing an error during rethrows

```js
import { Err, OnErr } from '@lvigil/err'

function loadPayload(file) {
  try {
    const payload = JSON.parse(fs.readFileSync(file, 'utf8'))
    if (Object(payload) !== payload) {
      throw Err('invalid payload', { payload })
    }
    return payload

  } catch (e) {
    throw OnErr(e, { file }).m('load payload failed')
  }
}
```

Result:

```js
{
  message: 'invalid payload',
  msgs: ['invalid payload', 'load payload failed'],
  original: { payload, file },
  stack: '...'
}
```

`OnErr` preserves:

- existing `message`
- existing `stack`
- existing `msgs` (appends new ones)
- existing `original` (merges new context, old values win)

### Using `.m()` for message breadcrumbs

`.m()` appends a message to `err.msgs[]` without changing `err.message`.

```js
// Deep in the call stack
throw Err('ENOENT: file not found')

// Middle layer
catch (e) {
  throw OnErr(e, { configPath }).m('failed to read config')
}

// Top layer
catch (e) {
  throw OnErr(e).m('app initialization failed')
}
```

Final `msgs`:

```js
['ENOENT: file not found', 'failed to read config', 'app initialization failed']
```

---

## API

### `Err(message, [context_dict], [flag_dict])`

Creates an enhanced `Error`.

| Parameter | Type | Description |
|-----------|------|-------------|
| `message` | `string` | Error message |
| `context_dict` | `object` | Debugging context (key-value pairs) |
| `flag_dict` | `object \| string` | Rarely needed. Only when caller checks `err.code` |

**`context_dict` must be an object** — it's a map of key-value pairs, not just values:

```js
// Correct — key tells you what the value means
throw Err('load failed', { file, userId })
// → context: { file: '/data/x.json', userId: 123 }

// Wrong — just a value, no key
throw Err('load failed', file)
// → context: '/data/x.json'  ← what is this? no one knows when logging
```

Returns: `Error` with `msgs`, `original`, and `.m()` method.

**Using `flag_dict`** — when you need a flag but no context:

```js
// No context needed, just a flag for the caller to check
throw Err('rate limit exceeded', null, { code: 'E_RATE_LIMIT' })

// Caller can then:
if (err.code === 'E_RATE_LIMIT') {
  await sleep(1000)
  retry()
}
```

**Key-mirror shorthand** — pass a string instead of `{ key: 'key' }`:

```js
// These are equivalent:
throw Err('rate limit exceeded', null, { E_RATE_LIMIT: 'E_RATE_LIMIT' })
throw Err('rate limit exceeded', null, 'E_RATE_LIMIT')

// Caller can then:
if (err.E_RATE_LIMIT) {
  await sleep(1000)
  retry()
}
```

### `OnErr(err, [context_dict], [flag_dict])`

Wraps/enhances an existing error.

| Parameter | Type | Description |
|-----------|------|-------------|
| `err` | `any` | The error to wrap (will be converted to Err) |
| `context_dict` | `object` | Additional context to merge (key-value pairs) |
| `flag_dict` | `object \| string` | Rarely needed. Only when caller checks `err.code` |

Returns: The same error instance, enhanced.

- If `err` is not an `Error`, it becomes one
- `msgs` is initialized from `err.message` if not present
- `.m()` method is added if not present

### `.m(message)`

Appends a message to `err.msgs[]`.

Returns: The error instance (for chaining).

```js
throw OnErr(e, { file }).m('load failed')
```

### `.f(flag_dict)`

Attaches flags to the error. Same interface as the `flag_dict` parameter in `Err()` and `OnErr()`.

| Parameter | Type | Description |
|-----------|------|-------------|
| `flag_dict` | `object \| string` | Flags to attach (or key-mirror string) |

Returns: The error instance (for chaining).

```js
throw OnErr(e).f('E_TIMEOUT')
throw OnErr(e).f({ code: 'E_TIMEOUT', retry: true })
```

Most of the time, passing `flag_dict` directly to `Err()` or `OnErr()` is sufficient. Use `.f()` when you need to add flags in a wrapper that doesn't accept `flag_dict`.

### `.c(context_dict)`

Merges context into `err.original`. Same interface as the `context_dict` parameter in `Err()` and `OnErr()`.

| Parameter | Type | Description |
|-----------|------|-------------|
| `context_dict` | `object` | Context to merge (old wins) |

Returns: The error instance (for chaining).

```js
throw OnErr(e).c({ userId, file })
```

Most of the time, passing `context_dict` directly to `Err()` or `OnErr()` is sufficient. Use `.c()` when you need to add context in a wrapper that doesn't accept `context_dict`.

---

## Protected Properties

These properties cannot be overwritten via `flag_dict`:

| Property | Reason |
|----------|--------|
| `name`, `message`, `stack`, `cause` | Standard Error properties |
| `msgs`, `original`, `m`, `f`, `c` | Core functionality of this library |
| `response` | Protected for compatibility with HTTP libraries (superagent, axios) |

---

## Example: cause chain vs flat

Run the examples to see the difference:

```sh
npm run example-cause   # ES2022 cause chain
npm run example-onerr   # This library's flat approach
```

**cause chain** — 4 layers = 4 nested stacks, mostly redundant:

```log
[16:57:39.242] ERROR (cause-chain): cause chain example
    err: {
      "message": "Request failed",
      "stack":
          Error: Request failed
              at handleRequest (example/cause-chain/api-handler.js:9:11)
              at run.js:6:3
      "cause": {
        "message": "Authentication failed",
        "stack":
            Error: Authentication failed
                at authenticate (example/cause-chain/auth-service.js:9:11)
                at handleRequest (example/cause-chain/api-handler.js:7:5)
                at run.js:6:3
        "cause": {
          "message": "Cannot find user 123",
          "stack":
              Error: Cannot find user 123
                  at findUser (example/cause-chain/user-repo.js:9:11)
                  at authenticate ...
                  at handleRequest ...
          "cause": {
            "message": "ECONNREFUSED 127.0.0.1:3306",
            "stack":
                Error: ECONNREFUSED 127.0.0.1:3306
                    at connect (example/cause-chain/db.js:4:9)
                    at findUser ...
                    at authenticate ...
                    at handleRequest ...
          }
        }
      }
    }
```

**OnErr flat** — 1 stack, all context merged:

```log
[16:58:56.394] ERROR (onerr-flat): OnErr flat example
    err: {
      "message": "ECONNREFUSED",
      "stack":
          Error: ECONNREFUSED
              at connect (example/onerr-flat/db.js:6:9)
              at findUser (example/onerr-flat/user-repo.js:8:5)
              at authenticate (example/onerr-flat/auth-service.js:8:5)
              at handleRequest (example/onerr-flat/api-handler.js:8:5)
              at run.js:6:3
      "msgs": [
        "ECONNREFUSED",
        "Cannot find user",
        "Authentication failed",
        "Request failed"
      ],
      "original": {
        "endpoint": "/api/auth",
        "token": "abc",
        "userId": 123,
        "host": "127.0.0.1",
        "port": 3306
      }
    }
```

Same information, half the noise. Context tells you everything at a glance.

---

## Philosophy

This library does **not** replace JavaScript's error system.
It adds what real-world debugging needs:

- **Flat, not nested** — everything in one place for easy logging
- **Message history** — see how the error bubbled up
- **Context accumulation** — see the state at each layer
- **Coding-friendly** — `if (err.code === 'E_TIMEOUT')` just works
- **Minimal and predictable** — no prototype hacks, no magic

The goal is **clarity** — not complexity.

---

## License

MIT © Ben P.P. Tung