0.0.1 • Published 5 years ago

klaxon v0.0.1

Weekly downloads
-
License
MIT
Repository
-
Last release
5 years ago

Klaxon

Klaxon is a framework for producing useful error messages.

Features

Error Types

Every error has a type.

Verbose Error Messages with Markdown

Additive Context

If you have a complex application with a deep class tree, it can be difficult to provide a user or developer with enough detail in the error to figure out exactly where it came from.

Take, for example, a "simple" data-oriented application:

  • The application connects to multiple databases
  • Each database has multiple tables or collections
  • Each table or collection has a schema
  • Each schema has multiple columns or fields
  • Each column or field has multiple validators
  • Each validator can throw one or more types of exceptions

You can try to get information about each of those things into the error messages that you throw, or you can use your class hierarchy to add the appropriate information as needed. Klaxon has a throw method that makes this easy. To use it, you just make sure you have a klaxon instance available in whatever your top-level class is, then you add a throw method to that class that adds whatever information is necessary and then calls the throw method in the next higher level up the tree. I recommend adding two methods, an instance method and a static method.

So your top-level class might look like this:

import path from 'path';
import klaxon from 'klaxon';

klaxon.load( path.resolve( __dirname, 'errors' ) );

class Application {
  throw( ...args ) {
    this.constructor.throw( ...args, {
      _thrower : this.throw
    } )
  }
  static throw( ...args ) {
    klaxon.throw( ...args, {
      _thrower : this.throw
      class    : 'Application',
    } )
  }
}

The way this works is that each throw method in the chain takes an array of objects that provide additional information about the error, and then adds on an object of it's own with additional information.

In our validation example we might continue on to do something similar for other types of classes in our tree:

class Database {
  throw( ...args ) {
    this.constructor.throw( ...args, {
      _thrower    : this.throw,
      database    : this.name,
    } );
  }
  static throw( ...args ) {
    this.application.throw( ...args, {
      _thrower    : this.throw,
      class       : 'Database',
    } );
  }
}

class Collection {
  throw( ...args ) {
    this.constructor.throw( ...args, {
      _thrower    : this.throw,
      collection  : this.name,
    } );
  }
  static throw( ...args ) {
    this.database.throw( ...args, {
      _thrower    : this.throw,
      class       : 'Collection',
    } );
  }
}

class Field {
  throw( ...args ) {
    this.constructor.throw( ...args, {
      _thrower    : this.throw,
      field       : this.name,
    } );
  }
  static throw( ...args ) {
    this.collection.throw( ...args, {
      _thrower    : this.throw,
      class       : 'Field',
    } );
  }
}

class Validator {
  throw( ...args ) {
    this.constructor.throw( ...args, {
      _thrower    : this.throw,
      validator   : this.name,
    } );
  }
  static throw( ...args ) {
    this.field.throw( ...args, {
      _thrower    : this.throw,
      class       : 'Validator',
    } );
  }
}

Notice that each class adds similar information through it's added object. Every one of these throw methods includes an object with a _thrower property that points to the method itself. There are several things that are important about this structure:

Errors propagate up the tree

Each method calls another throw method higher up in the object tree. This allows more information to be added before the error gets displayed. Also notice that in every case we add static information (such as class name) from the static method, and instance information (such as instance name) in the instance method. We then have the static method call the next higher throw method ine tree, while the instance method always calls this.constructor.throw. This means that you get more detailed instance information when you can (when the exception is thrown in the context of a specific instance) but you also still get useful information for errors thrown with the static method.

throw methods just add to a list of properties

Each method uses the same call signature, by passing the next throw method all of the arguments that it has received, and then adding another plain object to the end of that, with it's own information added in. It's important to maintain this structure, as the klaxon.throw method will collapse these objects down into a single set of properties, and the first value set for a given property will win. This means, for example that if you have built up objects like this:

const application = new Application( { name : 'my-app' } );
const database = new Database( { name : 'datastore', application } );
const collection = new Collection( { name : 'users', database } );
const field = new Field( { name : 'email', collection } );
const validator = new Validator( { name : 'validate-email', field } );

Then, when you throw an error from deep in that structure:

validator.throw( 'invalid-email' );

The resulting error will include these properties:

{
  validator   : 'validate-select',
  class       : 'Validator',
  field       : 'email',
  collection  : 'users',
  database    : 'datastore',
  application : 'my-app',
}

But you can call any of those other throw methods and they will get as much information added to the error as possible.

throw methods add similar information

You might have noticed that the information added to the exception was very similar for each class. The static methods always added the name of the class, and the instance methods added the name of the instance.

This gives you a good basic structure for determining exactly what the context was surrounding the error, but you can also add additional properties as needed, either in these throw methods, or directly when you call one of these methods.

Throwing an Error

Once you have setup your tree of throw methods, instances of objects that have these methods can very easily throw exceptions with a great amount of detail:

class SomeThing {
  constructor( opt ) {
    if ( typeof opt !== "string" ) {
      this.throw( 'param-error', "Argument must be string", {
        method    : 'constructor',
        argument  : 'opt',
        value     : opt,
        expected  : 'string',
        received  : typeof opt,
      } );
    }
  }
}

Arguments to throw

When you call one of these throw methods to create an actual error, there are several types of values you can provide that are supported. You should only use these argument shortcuts when calling throw from within regular code, when calling throw from within another throw method to add additional properties, you should always add a plain object with the properties you need added.

String (type or message)

String arguments are a little more complicated than other argument types discussed below, because a string can be either a type or a message. If a string is encountered that is a valid message name (meaning it matches /^[\w\-]+$/) and the error has not already been assigned a type then the string will be treated as as a type, otherwise it will be treated as a message.

This allows you to more easily throw errors with a type and a message, which are the most common arguments provided to the throw method:

this.throw( 'invalid-param', "The 'email' parameter must be a string" );
this.throw( 'unknown-option', `The '${key}' option is unknown` );

You can avoid the ambiguity between type and message by providing them in an object instead:

this.throw( {
  _type    : 'broken-stuff',
  _message : 'You broke something',
} );

Error Object

If the argument list contains an object that is an instance of Error then it will be added to an array of errors (in case there was more than one error encountered). The first error instance encountered in the argument list will also be assigned to the cause property, to indicate that it was likely the cause of the error being thrown.

Plain Object

type

An error type must consist of only letters, numbers, underscore or dash characters (/^\w\-]+$/), and an error can have only a single type. So, when determining if a string is a type or a message Klaxon considers if it matches the requirements for a type, and also if a type has already been parsed from the argument list.

message

Unlike the type parameter, a message has no restrictions beyond being a string, and you can have more than one message parameter provided to the same error. Multiple message values will be joined with newlines and form a multi-line error message.