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.
pnpm add @lvigil/errThis 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.
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.
import { Err } from '@lvigil/err'
throw Err('Invalid config', { config })One line — context, message, done:
import { OnErr } from '@lvigil/err'
catch (e) {
throw OnErr(e, { userId, file }).m('Failed to load user data')
}import { Err } from '@lvigil/err'
throw Err('invalid payload format', { payload })Result:
{
message: 'invalid payload format',
msgs: ['invalid payload format'],
original: { payload },
stack: '...'
}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)
.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']Creates an enhanced Error.
| Parameter | Type | Description |
|---|---|---|
message |
string |
Error message |
context_dict |
object |
Debugging context (key-value pairs) |
flag_dict |
object |
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()
}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 |
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
Appends a message to err.msgs[].
Returns: The error instance (for chaining).
throw OnErr(e, { file }).m('load failed')These properties cannot be overwritten via flag_dict:
| Property | Reason |
|---|---|
name, message, stack, cause |
Standard Error properties |
msgs, original, m |
Core functionality of this library |
response |
Protected for compatibility with HTTP libraries (superagent, axios) |
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.
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.
MIT © Ben P.P. Tung