npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

exceptionbag

v1.1.0

Published

Error composition and utility library

Downloads

10

Readme

ExceptionBag

GitHub Workflow Status (with event) npm version

Node.js package for easier error composition and debugging.

Provides ExceptionBag type of Error class that allows adding metadata to errors and chaining the errors to create more descriptive messages about the error failure flow.

Motivation for needing such a library is in cases when you are dealing with an older library that returns errors through callbacks, thus losing the stack trace you end up getting a vague error that tells you nothing.

// Somewhere in your method
oldLibrary.execute(id, (data, err) => {
  if(err) {
    cb(undefined, err);
    return;
  }
  
  cb(data);
})

The error above gets propagated via callbacks will have the internal stack of the library but not where it happened in your app and the message will be vague. Solving it with ExceptionBag would look like this:

oldLibrary.execute(id, (data, err) => {
  if(err) {
    cb(ExceptionBag
            .from('failed executing old library', err)
            .with({id})
            .captureStackTrace() // This method call is only required when the error is not thrown 
    );
    return;
  }
  
  cb(data);
})

Later on during when the callbacks stop and so you get into Promise based world you can stop wrapping them and catch them with all the metadata accumulated along the way.

try {
  await myOperation(id);
} catch (error) {
  if(error instanceof ExceptionBag) {
    console.log(error.message, error.getBag());
    // console.log(error.stack) you can also access the stack trace
    // console.error(error.cause) and the original error that caused the bubbling up
  } 
}

You would get:

failed executing old library: ECONNRESET unable to connect { id: 1}

and you can chain these as much as you want to get the best descriptive message where what happened in your code.

Install

npm install --save-exact exceptionbag@latest

Usage

Basic

ExceptionBag is meant to be used as a wrapper for Error or CustomError classes while extending in case of ExceptionBag.

import {ExceptionBag} from 'exceptionbag';

const getUser = async (userId) => {
  try {
    // fetch user
  } catch (error) {
    throw ExceptionBag.from('failed fetching user from database', error)
      .with('userId', userId);
  }
}

const doSomeBusinessLogic = async (userId, membership) => {
  try {
    // handle some business logic with user's membership
  } catch (error) {
    throw ExceptionBag.from('failed some business logic', error)
      .with('userId', userId)
      .with('membership', membership);
  }
}

try {
  await doSomeBusinessLogic(1234, 'standard')
} catch (error) {
  if (error instanceof ExceptionBag) {
    console.log(error.message, error.getBag());
  } else {
    console.log(error);
  }
}
// This will produce an error message:
// "failed some business logic: failed fetching user from database: Error Something failed"
// and log the metadata:
// { userId: 1234, membership: 'standard' }

Annotations

For simple use cases, annotations can be used to decorate the method

import {ThrowsExceptionBag} from "exceptionbag/decorators";

class MyService {

  @ThrowsExceptionBag('failed some business logic') // No message will add the class name and method name as reference
  async doSomeBusinessLogic(@InBag('userId') userId, @InBag('membership') membership) { // @InBag decorators adds key and value to the error bag
    // handle some business logic with user's membership
  }
}

This is identical to:

const doSomeBusinessLogic = async (userId, membership) => {
  try {
    // handle some business logic with user's membership
  } catch (error) {
    throw ExceptionBag.from('failed some business logic', error)
      .with('userId', userId)
      .with('membership', membership);
  }
}

It is also possible to ignore certain errors and propagate them further

import {ThrowsExceptionBag} from "exceptionbag/decorators";

class CustomError extends Error {
  public constructor(msg?: string) {
    super(msg);
    this.name = CustomError.name;
  }
}

class MyService {

  @ThrowsExceptionBag({ignore: CustomError}) // Re-throw CustomError instead of wrapping
  async doSomeBusinessLogic(userId, membership) {
    // handle some business logic with user's membership
  }
}

This is identical to:

const doSomeBusinessLogic = async (userId, membership) => {
  try {
    // handle some business logic with user's membership
  } catch (error) {
    if (error instanceof CustomError) {
      throw error;
    }
    throw ExceptionBag.from('failed some business logic', error)
      .with('userId', userId)
      .with('membership', membership);
  }
}

Custom decorators

You can create a custom decorator easily with the use of a helper function:

export function ThrowsCustomExceptionBag<T extends Constructable>(message?: string | ThrowsOptions<T>): DecoratedFunc {
  return createExceptionBagDecorator(CustomExceptionBag.from)(message);
}

And use it in same manor:

class MyHandler {
  
  @ThrowsCustomExceptionBag()
  doWork() {
    
  }
}

Usage as Nest.js filter

Ensure that you create a Nest.js filter to catch these errors and properly handle them.

@Catch(ExceptionBag)
class ExceptionBagFilter implements ExceptionFilter {
  catch(exception: ExceptionBag, host: ArgumentsHost): any {
    const ctx = host.switchToHttp();
    const res = ctx.getResponse<Response>();
    const req = ctx.getRequest<Request>();

    // Your custom logger
    console.log({
      message: exception.message,
      name: exception.name,
      stack: exception.stack,
      details: exception.getBag()
    });

    // You can check for specific error classes that extend the ExceptionBag if needed so

    res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({
      statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
      message: HttpStatus[HttpStatus.INTERNAL_SERVER_ERROR],
      timestamp: new Date().toISOString(),
      path: req.url,
    });
  }
}

Extending

You can always extend the class when you want different type of handling.

import { ExceptionBag } from 'exceptionbag';

class CustomExceptionBag extends ExceptionBag {
  public responseStatus: number;

  public constructor(msg: string, responseStatus: number, cause?: Error) {
    super(msg, cause);
    this.responseStatus = responseStatus;
    this.name = CustomExceptionBag.name;
  }
}

// The later use it
try {
  // ...
  throw CustomExceptionBag.from('custom failure', new Error('failure')).with({ status: 303 });
} catch (error) {
  if(error instanceof CustomExceptionBag) {
    // ... check response status
  }
}

And even create your own decorators for that class in the following way:

import { createExceptionBagDecorator } from 'exceptionbag/decorators';

function ThrowsCustomExceptionBag<T extends Constructable>(options?: ThrowsOptions<T>): DecoratedFunc;
function ThrowsCustomExceptionBag(message?: string): DecoratedFunc;
function ThrowsCustomExceptionBag<T extends Constructable>(message?: string | ThrowsOptions<T>): DecoratedFunc {
  return createExceptionBagDecorator(CustomExceptionBag.from.bind(CustomExceptionBag))(message);
}

// And then use it
class BusinessClass {
  @ThrowsCustomExceptionBag('failed doWork')
  doWork(@InBag('value') value) {
    // some work...
  }
}

Extensions

AxiosExceptionBag

Detects and wraps axios error, along with some request and response information like status, baseUrl, source, timeout, method, headers, responseData, etc.

import {AxiosExceptionBag, ExceptionBag} from 'exceptionbag';

try {
  // axios.get request
} catch (error) {
  throw AxiosExceptionBag.from('failed request x', error);
}

Later can be used to extract details:

import {AxiosExceptionBag} from "exceptionbag";

try {

} catch (error) {
  if (error instanceof AxiosExceptionBag) {
    if (error.hasStatus(400)) {
      const response = error.getResponseData<{ message: string; code: number }>();
      // handle response data
    } else if (error.status > 400) {
      // other type of handling
    }
  }
}

Supports @ThrowsAxiosExceptionBag decorator

import {ThrowsAxiosExceptionBag} from "exceptionbag/decorators";

class MyApiHandler {
  @ThrowsAxiosExceptionBag()
  async getData(@InBag('userId') userId): Promise<any> {
    // fetch data
  }
}

So when you are handling the error it's much easier to debug or handle specific cases

try {
    const data = await new MyApiHandler().getData('1234');
} catch (error) {
    if(error instanceof AxiosExceptionBag) {
        // has more type safe methods to easy handling
        const response = error.getResponseData<any>();
        const isBadRequest = error.hasStatus(400);
        const headers = error.getHeaders();
        // and the base getBag() with all key - value details
        const bag = error.getBag();
    }
}

Publishing package

Before publishing always ensure you ran the following check which also builds:

npm run check

Ensure that your merge requests or commits have the following prefixes in their message/title:

To create a patch, use a commit message like:

fix: testing patch releases

To create a minor release, use a commit message like:

feat: testing minor releases

Or, for a breaking change:

feat: testing major releases

BREAKING CHANGE: This is a breaking change.

These messages will instruct the semantic releaser to update to appropriate semantic version.

Changelog

Review changelog for releases at CHANGELOG.md.