@lvigil/err
v1.3.0
Published
Enhanced Error with message history, context dict, and flag dict — flat, not chained, designed for logging.
Maintainers
Readme
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
pnpm add @lvigil/errThe 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 bottomoriginal{}— one object, all context mergedflag_dict— directly onerr, 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
import { Err } from '@lvigil/err'
throw Err('Invalid config', { config })Wrap and rethrow
One line — context, message, done:
import { OnErr } from '@lvigil/err'
catch (e) {
throw OnErr(e, { userId, file }).m('Failed to load user data')
}Usage
Creating an error
import { Err } from '@lvigil/err'
throw Err('invalid payload format', { payload })Result:
{
message: 'invalid payload format',
msgs: ['invalid payload format'],
original: { payload },
stack: '...'
}Enhancing an error during rethrows
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:
{
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.
// 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:
['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:
// 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 loggingReturns: Error with msgs, original, and .m() method.
Using flag_dict — when you need a flag but no context:
// 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' }:
// 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
erris not anError, it becomes one msgsis initialized fromerr.messageif not present.m()method is added if not present
.m(message)
Appends a message to err.msgs[].
Returns: The error instance (for chaining).
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).
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).
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:
npm run example-cause # ES2022 cause chain
npm run example-onerr # This library's flat approachcause chain — 4 layers = 4 nested stacks, mostly redundant:
[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:
[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
