Handle exceptions smart β context-preserving, chainable, serializable errors for TypeScript.
Lightweight error handling with metadata embedding, namespace categorization, tag inheritance, and JSON round-tripping.
π¨ Need beautiful error rendering? Colorized stack traces, syntax-highlighted source code, and YAML metadata display are available in the companion package sher.log.
# npm
npm install xception
# pnpm
pnpm add xception
# yarn
yarn add xceptionimport { Xception } from 'xception';
throw new Xception('Payment failed', {
severity: 'error',
code: 'billing:payment_failed',
namespace: 'billing',
meta: { orderId: 'ORD-123', amount: 99.99 },
tags: ['payment', 'retryable'],
});class DatabaseError extends Xception {
constructor(query: string, cause: unknown) {
super('Database query failed', {
cause,
severity: 'error',
code: 'app:database_query_failed',
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));
}
// {
// "severity": "error",
// "code": "app:database_query_failed",
// "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"]
// }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
Xception preserves everything:
- π― Context preserved: Attach
metawith runtime state at the point of failure - π Chains maintained:
causeproperty links errors into full causality chains (TC39 aligned) - π JSON-ready:
toJSON()andfromJSON()round-trip error graphs for structured logging and replay - π·οΈ Categorized:
namespaceandtagslet you filter, route, and aggregate errors - π¦ Routable:
severityandcodesupport machine-readable handling, alerting, and i18n - π¦ Lightweight: Minimal footprint with a single types-only dependency
| 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 |
| Severity & Code | β | β | Route, classify, and translate errors |
| 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
import { Xception } from 'xception';
try {
// Some operation that fails
throw new Error('Network timeout');
} catch (cause) {
throw new Xception('API request failed', {
cause,
severity: 'warning',
code: 'api:network_timeout',
namespace: 'api:client',
meta: { endpoint: '/users', timeout: 5000 },
tags: ['network', 'retryable'],
});
}// Build a typed hierarchy for your domain
class AppError<
Meta extends Record<string, unknown> = Record<string, unknown>,
> extends Xception<Meta> {}
class DatabaseError extends AppError<{
query: string;
retryable: boolean;
}> {
constructor(query: string, cause: unknown) {
super('Database query failed', {
cause,
severity: 'error',
code: 'app:database_query_failed',
namespace: 'app:database',
meta: { query, retryable: true },
tags: ['database', 'recoverable'],
});
}
}
class ValidationError extends AppError<{
field: string;
value: unknown;
timestamp: number;
}> {
constructor(field: string, value: unknown) {
super(`Validation failed for field: ${field}`, {
namespace: 'validation',
severity: 'warning',
code: 'validation:invalid_field',
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']
}
}Use createXceptionClass() when you want typed subclasses with class-level defaults and instanceof support across duplicated package installations:
import { Xception, createXceptionClass } from 'xception';
const NotFoundError = createXceptionClass<{ resource: string }>(
'NotFoundError',
{
code: 'app:not_found',
severity: 'warning',
},
);
const UserNotFoundError = createXceptionClass<{ userId: string }>(
'UserNotFoundError',
{
base: NotFoundError,
code: 'app:user_not_found',
},
);
const error = new UserNotFoundError('User not found', {
meta: { userId: '42' },
});
console.log(error instanceof Xception); // true
console.log(error instanceof NotFoundError); // true
console.log(error instanceof UserNotFoundError); // true
console.log(error.severity); // 'warning'
console.log(error.code); // 'app:user_not_found'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 + deduplicatedSeverity defaults to error. It inherits through Xception cause chains unless you explicitly override it. Codes stay local to the error that declares them.
const inner = new Xception('Token expired', {
severity: 'warning',
code: 'auth:token_expired',
});
const outer = new Xception('Request rejected', {
cause: inner,
code: 'api:request_rejected',
});
console.log(outer.severity); // 'warning'
console.log(outer.code); // 'api:request_rejected'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'],
});
}Use the factory option to produce your own Xception subclass from xception():
class HttpError extends Xception {}
const originalError = new Error('Request failed');
const error = xception(originalError, {
namespace: 'http',
factory: (message, options) => new HttpError(message, options),
});
error instanceof HttpError; // trueimport { 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 });
}Reconstruct structured payloads from other services or log replays back into live Xception instances:
import { Xception, createXceptionClass } from 'xception';
const AuthError = createXceptionClass('AuthError', {
namespace: 'auth',
});
const payload = new AuthError('Authentication failed').toJSON();
const revived = Xception.fromJSON(payload, {
reviver: (json, defaults) =>
json.name === 'AuthError'
? new AuthError(json.message as string, defaults)
: undefined,
});
console.log(revived instanceof Xception); // true
console.log(revived instanceof AuthError); // true when revived by the reviver
console.log(revived.cause); // recursively reconstructednew Xception<Meta>(message: string, options?: XceptionOptions<Meta>)interface XceptionOptions<
Meta extends Record<string, unknown> = Record<string, unknown>,
> {
/** Upstream error to be embedded */
cause?: unknown;
/** Error namespace (e.g., 'app:database') */
namespace?: string;
/** Context data for debugging */
meta?: Meta;
/** Additional associations for filtering */
tags?: string[];
/** Severity for routing and alerting */
severity?: 'fatal' | 'error' | 'warning' | 'info' | 'debug';
/** Machine-readable error code */
code?: number | string;
}| Property | Type | Description |
|---|---|---|
cause |
unknown |
The upstream error |
namespace |
string | undefined |
Component identifier |
meta |
Meta |
Embedded context data |
tags |
string[] |
Associated tags (inherited + deduplicated from cause chain) |
severity |
Severity |
Alerting and routing level |
code |
number | string | undefined |
Machine-readable error identifier |
| Method | Returns | Description |
|---|---|---|
toJSON() |
JsonObject |
Serialize the entire error graph to JSON |
| Method | Returns | Description |
|---|---|---|
fromJSON(json, options?) |
Xception |
Rehydrate a serialized error graph to runtime instances |
interface FromJSONOptions {
/** Custom factory for producing subclasses */
reviver?: (
json: JsonObject,
defaults: XceptionOptions,
) => Xception | undefined;
/** Max cause chain depth (default: 50) */
maxDepth?: number;
}Xception.fromJSON() expects a non-null object with a string message. Missing or invalid optional fields fall back to sensible defaults:
namedefaults to'Xception'metadefaults to{}tagsdefaults to[]severitydefaults to'error'codedefaults toundefined
Cause reconstruction rules:
- object
causevalues are recursively deserialized asXception - string
causevalues becomenew Error(cause) - missing,
null, or unsupported cause shapes are dropped - recursive deserialization is truncated after
maxDepthlevels
When a reviver is provided, it is called cause-first for every object in the chain. Return a subclass instance to preserve domain-specific instanceof checks, or return undefined to fall back to the base Xception constructor.
Xception.fromJSON() throws TypeError when the root or a nested object does not contain a string message, or when a reviver returns a value that is not an Xception.
Create a branded Xception subclass with cascading defaults and cross-package instanceof support:
function createXceptionClass<
Meta extends Record<string, unknown> = Record<string, unknown>,
>(
name: string,
options?: CreateXceptionClassOptions<Meta>,
): XceptionConstructor<Meta> & {
readonly brand: symbol;
};
type XceptionConstructor<
Meta extends Record<string, unknown> = Record<string, unknown>,
> = new (
message: string,
options?: XceptionOptions<Meta>,
) => Xception<Meta>;
interface CreateXceptionClassOptions<
Meta extends Record<string, unknown> = Record<string, unknown>,
> extends XceptionOptions<Meta> {
base?: XceptionConstructor<any>;
}Class-level options become defaults for each instance. Subclasses created with base inherit their parent defaults unless they override them, and per-instance options always win. meta is replaced rather than deep-merged so each class keeps a clear typed metadata contract.
Convert any value to an Xception instance, preserving the original error's message, name, and stack:
function xception<
Meta extends Record<string, unknown> = Record<string, unknown>,
>(exception: unknown, options?: Options<Meta>): Xception<Meta>;
type Options<Meta extends Record<string, unknown>> = {
namespace?: string;
meta?: Meta;
tags?: string[];
severity?: 'fatal' | 'error' | 'warning' | 'info' | 'debug';
code?: number | string;
/** Custom factory for producing Xception subclasses */
factory?: (
message: string,
options: XceptionOptions<Meta>,
) => Xception<Meta>;
};When the input is already an Xception, metadata is merged (new meta overrides existing keys), tags are deduplicated, severity is inherited unless explicitly overridden, and code is not inherited. Note that xception() unwraps an existing Xception β the new instance's cause points to the original's upstream cause, not the Xception itself.
Recursively convert any value to a JSON-serializable structure. Handles circular references automatically with [circular reference as <path>] labels:
function jsonify(value: unknown): JsonValue;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;
};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 |
$severity |
Access error severity |
$code |
Access error code |
| 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.
| 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
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, $severity, $code) exist specifically for companion packages to access Xception's protected internals without requiring subclassing.
Walk the full cause chain programmatically:
let current: unknown = error;
while (current instanceof Xception) {
console.log(current.namespace, current.message);
current = current.cause;
}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 .]"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 duplicatesSee CONTRIBUTING.md for full guidelines.
- Fork & Clone:
git clone https://github.com/alvis/xception.git - Install:
pnpm install - Develop:
pnpm test:watchfor development mode - Test:
pnpm test && pnpm lint - Submit: Create a pull request
Code Style:
- Conventional Commits
- ESLint + Prettier enforced
- 100% test coverage required
Found a vulnerability? Please email alvis@hilbert.space with details. We aim to respond within 48 hours and patch as quickly as possible.
| 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 |
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
See CHANGELOG.md for version history and migration guides.
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.