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

exceptional.js

v0.6.2

Published

JavaScript exception handling library.

Downloads

727

Readme

exceptional.js

This is a library aimed at providing a simple error handling mechanism for usage in both Node.JS and in the browser.

Install using npm:

npm install exceptional.js

The code is written in TypeScript and better fits projects written in ES6 and above.

import * as exceptional from 'exceptional.js'

The library also addresses some of the other pain points of handling errors in node.js apps.

It comes with a model of transporting server side errors to the client in a sane manner, all exceptions have the same format.

Also, the library was built with localization in mind. For projects that have front-ends translated in multiple languages, errors should also take into account the language preference of the user, not just display error messages in english. This highly increases the user experience of such applications.

General usage

This library enforces a certain format for errors. Exceptions should be stateful and carry with them all the neccessary information to describe the error to a user.

/**
 * Exception interface.
 */
export interface IException <T> {
  namespace: string,
  code: number,
  payload: T
}

The code above is the generic structure of an exception. It contains information about the namespace (more on this further down), the code that uniquely identifies the error and a payload that can be anyting.

The library makes use of custom error objects that extend the Node.JS Error object.

All types of errors are constructed from a namespace and require the developer to pass in the code and a payload.

Built in exception class types are:

  1. Generic Exception

    This is the most basic type of error. It doesn't provide much info about what happened. Also, this is the type of error you'll be using when no other fits the use case.

    throw EXCEPTIONAL.GenericException(1, {debug: 'some-debug-info'});
  2. Domain Exception

    The name is borrowed from Domain Driven Design terminology. This type of exception is used for signaling an unmet requirement in the action that is performed. Let's say your app doesn't allow a user to create more than 5 resources on your server. This would be the exception to use when that user tries to create the 6th one.

    throw EXCEPTIONAL.DomainException(1, {debug: 'some-debug-info'});
  3. Conflict Exception

    Used to handle a case where there are conflicting resources. E.g. when a user tries to register with an already used email address.

     throw EXCEPTIONAL.ConflictException(1, {debug: 'some-debug-info'});
  4. Not Found Exception

    This exception is to be used when a client tries to access a resource that cannot be found on the server.

     throw EXCEPTIONAL.NotFoundException(1, {debug: 'some-debug-info'});
  5. Throttle Exception

    Used within mechanisms that limit the number of accesses to a certain resource found on the server

     throw EXCEPTIONAL.ThrottleException(1, {debug: 'some-debug-info'});
  6. Unauthorized Exception

    Used to handle unauthorized accesses to a server resource. Either the user is not logged in or does not have the neccessary permissions.

     throw EXCEPTIONAL.UnauthorizedException(1, {debug: 'some-debug-info'});
  7. Payment Required Exception

    Usefull for apps that have a payment system implemented for access to the resources. When a user tries to access his account and his subscription has expired, this type of exception is to be used.

     throw EXCEPTIONAL.PaymentRequiredException(1, {debug: 'some-debug-info'});
  8. Input Validation Exception

    This type of exception is used for signaling that the body of an HTTP request has a field with a different type than what the server was expecting. This type is a bit more special, because it requires that the payload contains an array with the fields that did not pass validation. It does so because that array can then be used in the client side code to display error messages next to a form's fields. As a best practice, the names of the error fields should match the name of the id assigned to the form control.

     throw EXCEPTIONAL.InputValidationException(1, {
       errors: [
         'email': 'Not a valid email address',
         'password': 'Should be at least 8 characters long'
       ]
     });

An error should contain a code (number) which distinctly identifies the sub type. For example, the NotFoundException class will be used for any resource accesses that point to a non existing resource, but the code can specify exactly which type of resource could not be found. Code number 1 could be used for a non existing user account, 2 for a missing comment with a certain ID and so on.

EXCEPTIONAL context

All the build in exceptions are only available through a CONTEXT object that is bound to a certain namespace. Tipically, a namespace would reflect a certain part of your application. Let's say we are tasked with building a notes app. We could use one namespace for the part of the app that handles user accounts and another for the code that processes notes.

Having multiple namespaces within your code, allows developers to reuse exception code numbers, thus avoiding reaching really high values for them.

| Namespace | Code | Description | | --------- | ---- | ------------------ | | users | 1 | Email already used | | users | 2 | Not a valid email | | users | 3 | Passwords do not match | | notes | 1 | Note title is required | | notes | 2 | Note body cannot exceed 256 characters |

Fully fuctioning example of the library usage

import { context } from 'exceptional.js'

const EXCEPTIONAL = context('users-namespace');

function foo () {
  if (true) {
    throw EXCEPTIONAL.GenericException(1, {message: 'something went wrong'});
  }
}

Async/Await usage

Exception handling is alot more easy when writting code using the async/await syntax. Code that is known to throw exceptions can be wrapped in try {} catch () {} blocks.

Writting code like this allows developers to make services that signal errors by throwing exceptions and only handle them at the top most level.

Handling HTTP API errors

This is where the fun part beggins. The library comes with a special class of exception named HttpException. This type of exception is particularly usefull for HTTP API servers. It encapsulates an HTTP status code and the error that ocurred.

export interface IHttpException {
  statusCode: number,
  error: IException <any>
}

Noticed the type of the error field? It's an IException<any>. ALWAYS! So how does it work?

let httpEx = new HttpException(err);

The HttpException class's constructor takes only 1 parameter. The exception that occurred in the running piece of code. The smart thing about it is that if typeof(err) is one of the built in exception classes, it will set the statusCode field to one of the standard HTTP status codes that match that type of exception.

| Exception Class | HTTP Status Code | | --------------------- | :--------------: | | GenericException | 500 | | DomainException | 403 | | ConflictException | 409 | | NotFoundException | 404 | | ThrottleException | 429 | | UnauthorizedException | 401 | | PaymentRequiredException | 402 | | InputValidationException | 400 |

If the err parameter is anything other than one of the built in exception class objects, the status code will default to 500 and the error field will have the code property set to 0, the namespace default and for the payload property, it will copy all of the enumerable properties of the original error.

Ok, ok, but where is the fun part? Well, let me tell you about one of my favourite node.js http frameworks: Express.js (duh).

Express comes with a really helpful (but highly unpopular) feature. You can define YOUR OWN ERROR MIDDLEWARE.

let app = express();
app.use(function defaultErrorHandler (err, req, res, next) {
  try {
    let httpEx = new HttpException(err);
    res.status(httpEx.statusCode).json(httpEx.error);
  } catch (err) {
    res.status(500).end();
  }
});

And now comes the punchline ... drumroll please.

app.post('/api/v1/user', async (req, res, next) => {
  try {
    await foo({email: req.body.email}); // method that can throw exceptions
    res.end();
  } catch (err) {
    next(err);
  }
});

When defining an express route middleware, we could pass for a callback an async function so that we can safely use try catch blocks. When an exception is thrown, the catch block simply calls the next middleware passing along the error object. When this happens, express will skipp all further middleware and execute the special error handler middleware, where we construct an HttpException from the received error object and everything else is handled by the library. The status code is automatically determined and error info is sent to the client.

TA DAAAA!! No more passing req and res objects down to controller methods to control the status code sent to the client.

Localization

The requirement that an exception must have a code property has two major benefits. First of all, it helps with code verbosity, writting a number is much eassier than an error string. And secondly, an error code can be matched to an error string.

exceptional.js provides an API for registering error code tables with multiple language support.

/**
 * Error table interface.
 */
export interface IErrorTable {
  namespace: string,
  locale: string,
  errors: {[key: number]: string}
}
import { registerTable } from 'exceptional.js';

// english error table
registerTable({
  namespace: 'default',
  locale: 'en',
  errors: {
    0: 'Something went wrong.',
    1: 'The email address ${email} is already registered.',
  }
});

// romanian error table
registerTable({
  namespace: 'default',
  locale: 'ro',
  errors: {
    0: 'Something went wrong',
    1: 'Adresa de email ${email} este deja folosita.',
  }
});

Displaying error messages

The locale must be set right after the error tables have been loaded into the library.

import { setLocale } from 'exceptional.js';

setLocale('en');

Keep in mind that the locale can be set to another value anytime.

Using the format method provided by the library, the error string matching the namespace and code of the passed exception object will be retrieved from the error table with the locale selected.

import { format } from 'exceptional.js';

console.log(format(err));

NOTE! If a string cannot be found in the table with that namespace, the library will try to find a table with the default value for the namespace field and search there. If still, no error string can be found it will throw an error.

The library also supports interpolation in the error strings by using the familiar syntax ${key}, where key is a property on the payload object of an exception class.

import { registerTable, context, setLocale } from 'exceptional.js';

registerTable({
  namespace: 'users',
  locale: 'en',
  errors: {
    0: 'Something went wrong.',
    1: 'The email address ${email} is already registered.',
  }
});

setLocale('en');

const EXCEPTIONAL = context('users');
try {
  throw EXCEPTIONAL.ConflictException(1, {
    email: '[email protected]'
  });
} catch (err) {
  console.error(format(err));
}

This will print in the error console the message:

The email address [email protected] is already registered.

Helpers

The library also comes with two more helper classes.

  1. ServerException

    This comes in handy when logging exceptions on the server

    app.use(function defaultErrorHandler (err, req, res, next) {
      try {
        let httpEx = new HttpException(err);
        res.status(httpEx.statusCode).json(httpEx.error);
    
        // log exceptions
        let serverException = new ServerException(httpEx.statusCode, httpEx.error);
        console.error(
          `<===== API_EXCEPTION =====>
           [ROUTE]
           Method: ${req.method}
           Url: ${req.url}
           Body: ${JSON.stringify(req.body)}
           [MESSAGE]
           ${serverException.message}
           [ORIGINAL]
           ${JSON.stringify(serverException.exception)}
           [STACK]
           ${(serverException.exception as any).stack}
           <================ END_EXCEPTION =================>
          `
        );
      } catch (err) {
        res.status(500).end();
      }
    });

    This class will automatically format() the exception and put the resulting error string in the message field.

    Also the stack property is inherited from the node.js Error class.

    The same class can be used on the client side code to handle error from HTTP requests made to the API server. The only difference is that the stack property is not transported to the client for obvious security reasons.

  2. ClientException

    Made for client side usage. It will wrap any programmer error that will have an error code of 0 and a namespace of default.

    import { ClientException, format } from 'exceptional.js';
    
    try {
      let obj = {};
      obj.foo.bar = 10; // will throw 'cannot access property bar of undefined'
    } catch (err) {
      let clientErr = new ClientException(err);
      console.error(format(clientErr.error)); // error field is an IException
    }

Final thoughts

Exception handling is one of the most underrated aspects amongst javascript developers and most of them fail to get it right. This hurts the lifecycle of big projects, but with the help of exceptional.js, hopefully developers will have the right tools to properly handle exceptions.

Happy coding!