Skip to content

Repository files navigation

error-extender

Simplifies creation of custom Error classes for Node.js, with cause-chaining stack traces (à la Java) and deep-merging defaults.

Why error-extender?

Modern JavaScript has Error.cause and class-based custom errors — but there are real gaps that error-extender fills.

Cause chains appear in logs without any custom formatter

Most loggers (Winston, Pino, etc.) and log aggregators serialize only error.stack. Native Error.cause is invisible to them unless you write a custom serializer for every tool in your pipeline. error-extender appends each Caused by: frame directly into the stack string, so the full chain appears wherever stack traces are written — no extra setup.

// Native — cause is invisible in error.stackconstroot=newError('connection refused');consterr=newError('query failed',{cause: root});console.error(err.stack);// Error: query failed// at Object.<anonymous> (/app/index.js:2:13)// ...// (cause never appears — most loggers stop here)// error-extender — full chain baked into the stack stringconst{ extendError }=require('error-extender');constDatabaseError=extendError('DatabaseError');constroot=newError('connection refused');consterr=newDatabaseError({message: 'query failed',cause: root});console.error(err.stack);// DatabaseError: query failed// at Object.<anonymous> (/app/index.js:8:13)// ...// Caused by: Error: connection refused// at Object.<anonymous> (/app/index.js:7:14)// ...

Typed, structured context on every error

Native Error has no data field. Without error-extender, attaching structured context (an HTTP status, a request ID, an affected resource) requires manual boilerplate on every custom class.

// Native — boilerplate repeated on every custom error classclassHttpErrorextendsError{readonlystatus: number;readonlybody?: string;constructor(message: string,status: number,body?: string){super(message);this.name='HttpError';this.status=status;this.body=body;}}// error-extender — typed data in one call, no boilerplateimport{extendError}from'error-extender';interfaceHttpErrorData{status: number;body?: string}constHttpError=extendError<HttpErrorData>('HttpError');consterr=newHttpError({data: {status: 404,body: 'Not Found'}});err.data?.status;// number — fully typed

Default values cascade and deep-merge down the hierarchy

Define defaultMessage and defaultData once at the parent level; child errors inherit them automatically. When both parent and child defaultData are plain objects, they deep-merge (child wins on conflict) — so a DatabaseError can inherit { status: 500 } from ServiceError and only override what it needs.

import{extendError}from'error-extender';constServiceError=extendError('ServiceError',{defaultMessage: 'A service error has occurred.',defaultData: {status: 500},});constDatabaseError=extendError('DatabaseError',{parent: ServiceError,// no defaultMessage — inherits from ServiceErrordefaultData: {message: 'A database error has occurred.'},});console.log(DatabaseError.defaultData);// { status: 500, message: 'A database error has occurred.' }// ^^ status inherited from ServiceError, message added by DatabaseErrorconsterr=newDatabaseError();// no args neededconsole.log(err.message);// 'A service error has occurred.' (inherited)console.log(err.data);// { status: 500, message: 'A database error has occurred.' }

Stack traces point at your code, not library internals

error-extender uses Error.captureStackTrace to remove its own frames from the stack, so every trace starts at the call site in your application.

// Without captureStackTrace — library internals pollute the top of the stack
Error: something went wrong
at new ExtendedErrorImpl (node_modules/error-extender/dist/index.js:18:5)
at Object.<anonymous> (/app/service.js:12:9) <-- your code, buried
...
// error-extender — trace starts directly at your call site
ServiceError: something went wrong
at Object.<anonymous> (/app/service.js:12:9) <-- your code, first
...

Install

npm install error-extender

Quick Start

TypeScript

import{extendError}from'error-extender';constCustomError=extendError('CustomError');constrootCause=newError('something broke deep down');thrownewCustomError({message: 'An error has occurred.',cause: rootCause});

Plain JS (CommonJS)

const{ extendError }=require('error-extender');constCustomError=extendError('CustomError');constrootCause=newError('something broke deep down');thrownewCustomError({message: 'An error has occurred.',cause: rootCause});

The thrown error's .stack will include the full cause chain:

CustomError: An error has occurred.
at Object.<anonymous> (/opt/app/index.js:7:7)
...
Caused by: Error: something broke deep down
at Object.<anonymous> (/opt/app/index.js:5:19)
...

Features

Creating Custom Error Classes

// TypeScriptimport{extendError}from'error-extender';constAppError=extendError('AppError');
// Plain JSconst{ extendError }=require('error-extender');constAppError=extendError('AppError');

The second argument accepts options:

keytypedescription
parentError constructor (or subclass of it)Parent error class to extend (default: Error)
defaultMessagestringFallback message when none is provided
defaultDataanyFallback data (deep-merged with instance data if both are plain objects)

Error Hierarchies

Custom errors can extend other custom errors. instanceof checks work across the full hierarchy.

// TypeScriptimport{extendError}from'error-extender';constAppError=extendError('AppError');constServiceError=extendError('ServiceError',{parent: AppError});constDatabaseError=extendError('DatabaseError',{parent: ServiceError});consterr=newDatabaseError();console.log(errinstanceofDatabaseError);// trueconsole.log(errinstanceofServiceError);// trueconsole.log(errinstanceofAppError);// trueconsole.log(errinstanceofError);// true
// Plain JSconst{ extendError }=require('error-extender');constAppError=extendError('AppError');constServiceError=extendError('ServiceError',{parent: AppError});constDatabaseError=extendError('DatabaseError',{parent: ServiceError});consterr=newDatabaseError();console.log(errinstanceofDatabaseError);// trueconsole.log(errinstanceofServiceError);// trueconsole.log(errinstanceofAppError);// trueconsole.log(errinstanceofError);// true

Default Message & Data Inheritance

defaultMessage and defaultData cascade down the hierarchy. Children inherit parent defaults and can override or extend them.

// TypeScriptimport{extendError}from'error-extender';constAppError=extendError('AppError',{defaultMessage: 'An unhandled error has occurred.',defaultData: {status: 503,message: 'Service unavailable.'},});constServiceError=extendError('ServiceError',{parent: AppError,defaultMessage: 'A service error has occurred.',defaultData: {status: 500,message: 'Internal server error.'},});constDatabaseError=extendError('DatabaseError',{parent: ServiceError,// no defaultMessage — inherits ServiceError'sdefaultData: {message: 'A database error has occurred.'},});console.log(DatabaseError.defaultData);// { status: 500, message: 'A database error has occurred.' }// ^^ status inherited from ServiceError, message overridden
// Plain JSconst{ extendError }=require('error-extender');constAppError=extendError('AppError',{defaultMessage: 'An unhandled error has occurred.',defaultData: {status: 503,message: 'Service unavailable.'},});constServiceError=extendError('ServiceError',{parent: AppError,defaultMessage: 'A service error has occurred.',defaultData: {status: 500,message: 'Internal server error.'},});constDatabaseError=extendError('DatabaseError',{parent: ServiceError,defaultData: {message: 'A database error has occurred.'},});console.log(DatabaseError.defaultData);// { status: 500, message: 'A database error has occurred.' }

Constructor Options

Custom errors accept a single options object:

keyaliastypedescription
messagemstringError message
datadanyArbitrary attached data
causecinstanceof ErrorThe underlying cause

Aliases (m, d, c) are evaluated first; if m is truthy it takes precedence over message.

// TypeScriptimport{extendError}from'error-extender';constServiceError=extendError('ServiceError');try{// ...something that throws}catch(err){thrownewServiceError({message: 'Failed to call downstream service.',data: {ref: '7e9f876ca116'},cause: errasError,});}
// Plain JSconst{ extendError }=require('error-extender');constServiceError=extendError('ServiceError');try{// ...something that throws}catch(err){thrownewServiceError({message: 'Failed to call downstream service.',data: {ref: '7e9f876ca116'},cause: err,});}

Instance Properties

In addition to the standard name, message, and stack:

propertydescription
dataThe resolved data (instance data deep-merged with defaultData when both are plain objects)
causeThe causing Error instance

Instance data Merges with defaultData

When both defaultData and the instance data are plain objects, they are deep-merged (instance values win on conflict).

// TypeScriptimport{extendError}from'error-extender';constAppError=extendError('AppError',{defaultData: {status: 503,message: 'Service unavailable.'},});consterr=newAppError({data: {status: 401}});console.log(err.data);// { status: 401, message: 'Service unavailable.' }// ^^ status overridden, message filled from defaultData
// Plain JSconst{ extendError }=require('error-extender');constAppError=extendError('AppError',{defaultData: {status: 503,message: 'Service unavailable.'},});consterr=newAppError({data: {status: 401}});console.log(err.data);// { status: 401, message: 'Service unavailable.' }

Cause Chain in Stack Traces

Each Caused by: section appends the full stack of the causing error, arbitrarily deep.

// TypeScriptimport{extendError}from'error-extender';constServiceError=extendError('ServiceError');constDatabaseError=extendError('DatabaseError',{parent: ServiceError});try{try{thrownewError('connection refused');}catch(root){thrownewDatabaseError({message: 'Query failed.',cause: rootasError});}}catch(dbErr){thrownewServiceError({message: 'Could not load user.',cause: dbErrasError});}
// Plain JSconst{ extendError }=require('error-extender');constServiceError=extendError('ServiceError');constDatabaseError=extendError('DatabaseError',{parent: ServiceError});try{try{thrownewError('connection refused');}catch(root){thrownewDatabaseError({message: 'Query failed.',cause: root});}}catch(dbErr){thrownewServiceError({message: 'Could not load user.',cause: dbErr});}

Stack trace output:

ServiceError: Could not load user.
at Object.<anonymous> (/opt/app/index.js:14:9)
...
Caused by: DatabaseError: Query failed.
at Object.<anonymous> (/opt/app/index.js:10:11)
...
Caused by: Error: connection refused
at Object.<anonymous> (/opt/app/index.js:7:11)
...

TypeScript: Typed data

Pass a type parameter to get full type safety on data and defaultData.

import{extendError}from'error-extender';interfaceHttpErrorData{status: number;body?: string;}constHttpError=extendError<HttpErrorData>('HttpError',{defaultData: {status: 500},});consterr=newHttpError({data: {status: 404,body: 'Not Found'}});console.log(err.data?.status);// 404 (typed as HttpErrorData)

100% Code Coverage

Test coverage is verified on every build via npm test.

License

Zero-Clause BSD License (0BSD)

Copyright (c) 2018 Joseph Baking

Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

About

TypeScript library for creating custom Error classes with typed data, cause chaining, and inheritance

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages