Skip to content
/ err Public

A context-rich, chain-friendly Error replacement with persistent debugging information.

Notifications You must be signed in to change notification settings

benpptung/err

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

34 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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/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_dictold wins, because the context closer to the error source is more valuable for debugging
  • flag_dictnew 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 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 logging

Returns: 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()
}

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 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).

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

Protected Properties

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)

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 approach

cause 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-friendlyif (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

About

A context-rich, chain-friendly Error replacement with persistent debugging information.

Resources

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •