klaxon v0.0.1
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.
5 years ago