Simplifies creation of custom Error classes for Node.js, with cause-chaining stack traces (à la Java) and deep-merging defaults.
Modern JavaScript has Error.cause and class-based custom errors — but there are real gaps that error-extender fills.
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)// ...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 typedDefine 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.' }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
...
npm install error-extenderTypeScript
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)
...
// TypeScriptimport{extendError}from'error-extender';constAppError=extendError('AppError');// Plain JSconst{ extendError }=require('error-extender');constAppError=extendError('AppError');The second argument accepts options:
| key | type | description |
|---|---|---|
parent | Error constructor (or subclass of it) | Parent error class to extend (default: Error) |
defaultMessage | string | Fallback message when none is provided |
defaultData | any | Fallback data (deep-merged with instance data if both are plain objects) |
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);// truedefaultMessage 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.' }Custom errors accept a single options object:
| key | alias | type | description |
|---|---|---|---|
message | m | string | Error message |
data | d | any | Arbitrary attached data |
cause | c | instanceof Error | The 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,});}In addition to the standard name, message, and stack:
| property | description |
|---|---|
data | The resolved data (instance data deep-merged with defaultData when both are plain objects) |
cause | The causing Error instance |
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.' }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)
...
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)Test coverage is verified on every build via npm test.
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.