Skip to content

alvis/xception

Logo

npm build coverage vulnerabilities dependencies license

Handle exceptions smart — context-preserving, chainable, serializable errors for TypeScript.

Lightweight error handling with metadata embedding, namespace categorization, tag inheritance, and JSON serialization.

🎨 Need beautiful error rendering? Colorized stack traces, syntax-highlighted source code, and YAML metadata display are available in the companion package sher.log.


⚡ Quick Start

# npm
npm install xception
# pnpm
pnpm add xception
# yarn
yarn add xception
import { Xception } from 'xception';

throw new Xception('Payment failed', {
  cause: originalError,
  namespace: 'billing',
  meta: { orderId: 'ORD-123', amount: 99.99 },
  tags: ['payment', 'retryable'],
});

With Custom Error Classes

class DatabaseError extends Xception {
  constructor(query: string, cause: Error) {
    super('Database query failed', {
      cause,
      namespace: 'app:database',
      meta: { query, retryable: true },
      tags: ['database', 'recoverable'],
    });
  }
}

try {
  // Your database operation
  throw new Error('Connection timeout');
} catch (error) {
  const dbError = new DatabaseError('SELECT * FROM users', error);
  console.log(JSON.stringify(dbError.toJSON(), null, 2));
}
// {
//   "namespace": "app:database",
//   "name": "DatabaseError",
//   "message": "Database query failed",
//   "stack": "DatabaseError: Database query failed\n    at ...",
//   "cause": { "message": "Connection timeout", "name": "Error", ... },
//   "meta": { "query": "SELECT * FROM users", "retryable": true },
//   "tags": ["database", "recoverable"]
// }

✨ Why Xception?

😩 The Problem

Standard JavaScript errors lose context as they propagate:

  • Context vanishes: throw new Error('Query failed') — which query? what parameters? what user?
  • Chains break: Wrapping errors with new Error('...') discards the original stack trace
  • No structure: JSON.stringify(new Error('fail')) gives you {} — useless for logging pipelines
  • No categorization: No standardized way to tag, namespace, or filter errors

💡 The Solution

Xception preserves everything:

  • 🎯 Context preserved: Attach meta with runtime state at the point of failure
  • 🔗 Chains maintained: cause property links errors into full causality chains (TC39 aligned)
  • 📊 JSON-ready: toJSON() serializes the entire error graph for structured logging
  • 🏷️ Categorized: namespace and tags let you filter, route, and aggregate errors
  • 📦 Lightweight: Minimal footprint with a single types-only dependency

🚀 Key Features

Feature Xception Standard Error Why It Matters
Context Embedding Capture runtime state when errors occur
Error Chaining Partial Maintain full causality with upstream errors
Metadata Support Embed any context for debugging
Namespace & Tags Categorize errors for filtering
JSON Serialization Ready for structured logging and monitoring
Tag Inheritance Tags propagate through cause chains
Circular-safe Handles circular references in serialization
TypeScript-first Partial Full type safety with generics

Core Benefits:

  • 🔍 Debug faster: Context-aware errors reduce investigation time — see exactly what went wrong and where
  • 🎯 Find root causes: Full error chains show the complete causality from origin to surface
  • 🛡️ Production-ready: Structured serialization for monitoring tools like Datadog, Sentry, and ELK
  • 📊 Smart logging: Tag and namespace-based filtering for different environments

📖 Usage Examples

Basic Error Wrapping

import { Xception } from 'xception';

try {
  // Some operation that fails
  throw new Error('Network timeout');
} catch (cause) {
  throw new Xception('API request failed', {
    cause,
    namespace: 'api:client',
    meta: { endpoint: '/users', timeout: 5000 },
    tags: ['network', 'retryable'],
  });
}

Custom Error Hierarchies

// Build a hierarchy for your domain
class AppError extends Xception {}

class DatabaseError extends AppError {
  constructor(query: string, cause: Error) {
    super('Database query failed', {
      cause,
      namespace: 'app:database',
      meta: { query, retryable: true },
      tags: ['database', 'recoverable'],
    });
  }
}

class ValidationError extends AppError {
  constructor(field: string, value: unknown) {
    super(`Validation failed for field: ${field}`, {
      namespace: 'validation',
      meta: { field, value, timestamp: Date.now() },
      tags: ['validation', 'user-error'],
    });
  }
}

// Narrow with instanceof
try {
  await queryDatabase(sql);
} catch (error) {
  if (error instanceof DatabaseError) {
    // Access typed metadata
    console.log(error.meta); // { query: '...', retryable: true }
    console.log(error.tags); // ['database', 'recoverable']
  }
}

Tag Inheritance

Tags automatically propagate and deduplicate through cause chains:

const inner = new Xception('disk full', {
  tags: ['infrastructure', 'retryable'],
});

const outer = new Xception('Write failed', {
  cause: inner,
  tags: ['storage'],
});

console.log(outer.tags);
// ['infrastructure', 'retryable', 'storage'] — inherited + deduplicated

Error Conversion with xception()

Convert any thrown value into an Xception — preserving the original message, name, and stack:

import { xception } from 'xception';

try {
  JSON.parse('invalid json');
} catch (error) {
  throw xception(error, {
    namespace: 'parser:json',
    meta: { source: 'user-input' },
    tags: ['parsing', 'recoverable'],
  });
}

Custom Factory Pattern

Use the factory option to produce your own Xception subclass from xception():

class HttpError extends Xception {}

const error = xception(originalError, {
  namespace: 'http',
  factory: (message, options) => new HttpError(message, options),
});

error instanceof HttpError; // true

Structured Logging Integration

import { Xception } from 'xception';

function errorMiddleware(
  error: Error,
  req: Request,
  res: Response,
  next: NextFunction,
) {
  const wrapped =
    error instanceof Xception
      ? error
      : new Xception(error.message, { cause: error, namespace: 'http' });

  // toJSON() gives you a complete, circular-safe JSON object
  logger.error(wrapped.toJSON());

  res.status(500).json({ error: wrapped.message });
}

🔧 API Reference

Class: Xception

new Xception(message: string, options?: XceptionOptions)
interface XceptionOptions {
  /** Upstream error to be embedded */
  cause?: unknown;
  /** Error namespace (e.g., 'app:database') */
  namespace?: string;
  /** Context data for debugging */
  meta?: Record<string, unknown>;
  /** Additional associations for filtering */
  tags?: string[];
}

Properties

Property Type Description
cause unknown The upstream error
namespace string | undefined Component identifier
meta Record<string, unknown> Embedded context data
tags string[] Associated tags (inherited + deduplicated from cause chain)

Methods

Method Returns Description
toJSON() JsonObject Serialize the entire error graph to JSON

Function: xception()

Convert any value to an Xception instance, preserving the original error's message, name, and stack:

function xception(exception: unknown, options?: Options): Xception;

type Options = {
  namespace?: string;
  meta?: Record<string, unknown>;
  tags?: string[];
  /** Custom factory for producing Xception subclasses */
  factory?: (message: string, options: XceptionOptions) => Xception;
};

When the input is already an Xception, metadata is merged (new meta overrides existing keys) and tags are deduplicated. Note that xception() unwraps an existing Xception — the new instance's cause points to the original's upstream cause, not the Xception itself.

Function: jsonify()

Recursively convert any value to a JSON-serializable structure. Handles circular references automatically with [circular reference as <path>] labels:

function jsonify(value: unknown): JsonValue;

Function: isErrorLike()

Type guard that checks if a value has the shape of an Error (has message, optional name and stack):

function isErrorLike(value: unknown): value is ErrorLike;

type ErrorLike =
  | Error
  | {
      message: string;
      name?: string;
      stack?: string;
      [key: string | symbol]: unknown;
    };

Symbols

These symbols provide direct access to Xception's protected internals. They exist so that companion packages (like sher.log) can read Xception properties without requiring subclassing:

Symbol Description
$namespace Access error namespace
$tags Access error tags
$cause Access error cause
$meta Access error metadata

🌐 Compatibility & Size

Requirement Value
Node.js ≥ 18.18
TypeScript 5.x+
Module format ESM only
Browsers Modern browsers
Dependencies type-fest (types only — zero runtime cost)

The package is tree-shakeable. Import only what you use — unused exports are eliminated by bundlers.


⚔️ Alternatives

Feature xception verror pretty-error ono
Error chaining
Context / meta
Namespace & tags
JSON serialization
TypeScript-first
Rendering Via sher.log
Active maintenance Limited

When to choose what:

  • xception — When you need rich error context, chaining, and structured serialization for production logging
  • Standard Error + cause — When you only need basic chaining (built-in since Node 16.9)
  • pretty-error — When you only need prettier console output without structured data

🔌 Ecosystem

xception is designed as a focused core with companion packages for extended functionality. The rendering layer was intentionally separated to keep the core lightweight.

Package Description
xception Context-aware error handling — metadata, chaining, serialization (this package)
sher.log Beautiful error rendering — colorized stack traces, source code display, YAML metadata

The exported symbols ($namespace, $tags, $cause, $meta) exist specifically for companion packages to access Xception's protected internals without requiring subclassing.


🏗️ Advanced Features

Error Chain Traversal

Walk the full cause chain programmatically:

let current: unknown = error;
while (current instanceof Xception) {
  console.log(current.namespace, current.message);
  current = current.cause;
}

Circular Reference Safety

jsonify() and toJSON() handle circular references gracefully — no JSON.stringify crashes:

const meta: Record<string, unknown> = { key: 'value' };
meta.self = meta; // circular!

const error = new Xception('fail', { meta });
console.log(error.toJSON());
// meta.self → "[circular reference as ..self]"

Tag Deduplication

When chaining errors, tags are automatically merged and deduplicated:

const inner = new Xception('root cause', { tags: ['infra', 'retryable'] });
const outer = new Xception('wrapper', {
  cause: inner,
  tags: ['retryable', 'critical'], // 'retryable' already exists on inner
});

console.log(outer.tags);
// ['infra', 'retryable', 'critical'] — no duplicates

🤝 Contributing

See CONTRIBUTING.md for full guidelines.

  1. Fork & Clone: git clone https://github.com/alvis/xception.git
  2. Install: pnpm install
  3. Develop: pnpm test:watch for development mode
  4. Test: pnpm test && pnpm lint
  5. Submit: Create a pull request

Code Style:


🛡️ Security

Found a vulnerability? Please email [email protected] with details. We aim to respond within 48 hours and patch as quickly as possible.


🛠️ Troubleshooting

Issue Solution
TypeScript errors Ensure TypeScript 5.x+ and "moduleResolution": "bundler" or "node16" in tsconfig
Cannot import (CJS) xception v9 is ESM-only; use dynamic import() in CommonJS or migrate to ESM
Tags not inherited Tag inheritance only works when cause is an Xception instance, not a plain Error
toJSON() missing properties Only metadata in meta is serialized; use meta for custom data, not ad-hoc error properties
Circular references in meta jsonify() handles circular refs automatically with [circular reference as <path>]
xception() changes error class name xception() preserves the original error's name and stack — this is intentional for debugging accuracy

❓ FAQ

Does xception replace the native Error cause? No. It aligns with the TC39 Error Cause proposal but adds namespace, meta, tags, and serialization on top.

Can I use xception in the browser? Yes — it works in any modern JavaScript environment that supports ESM. Rendering via sher.log is Node.js focused.

Is type-fest a runtime dependency? No. It provides TypeScript types only. There is zero runtime cost.

More help: GitHub Issues · Discussions


📜 Changelog

See CHANGELOG.md for version history and migration guides.


📄 License

MIT © 2020-2026 Alvis HT Tang

Free for personal and commercial use. See LICENSE for details.


⭐ Star on GitHub · 📦 View on npm · 📖 Documentation

Built for developers who refuse to lose context when things go wrong.

About

📍 An exceptional handy library for stack rendering and creating context-aware custom errors.

Topics

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors