├── .gitignore ├── .editorconfig ├── .travis.yml ├── karma.conf.js ├── LICENSE ├── test ├── typeforce.js ├── util.js └── index.js ├── lib ├── typeforce.js ├── util.js └── index.js ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | 4 | error-system.js 5 | npm-debug.log 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.json] 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - 0.10 5 | - 0.12 6 | - 4 7 | - 5 8 | addons: 9 | firefox: latest 10 | before_script: 11 | - sh -e /etc/init.d/xvfb start 12 | env: 13 | global: 14 | - DISPLAY=:99.0 15 | matrix: 16 | - TEST_SUITE=compile 17 | - TEST_SUITE=test 18 | matrix: 19 | include: 20 | - node_js: 4 21 | env: TEST_SUITE=coveralls 22 | - node_js: 4 23 | env: TEST_SUITE=lint 24 | script: npm run-script $TEST_SUITE 25 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.set({ 3 | frameworks: ['browserify', 'detectBrowsers', 'mocha'], 4 | files: ['test/*.js'], 5 | preprocessors: { 6 | 'test/*.js': ['browserify'] 7 | }, 8 | singleRun: true, 9 | plugins: [ 10 | 'karma-browserify', 11 | 'karma-chrome-launcher', 12 | 'karma-firefox-launcher', 13 | 'karma-detect-browsers', 14 | 'karma-mocha' 15 | ], 16 | browserify: { 17 | debug: true 18 | }, 19 | detectBrowsers: { 20 | enabled: true, 21 | usePhantomJS: false, 22 | postDetection: function (availableBrowser) { 23 | if (process.env.TRAVIS) { 24 | return ['Firefox'] 25 | } 26 | 27 | var browsers = ['Chrome', 'Firefox'] 28 | return browsers.filter(function (browser) { 29 | return availableBrowser.indexOf(browser) !== -1 30 | }) 31 | } 32 | } 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Fomichev Kirill 4 | 5 | Parts of this software are based on Bitcore 6 | Copyright (c) 2013-2015 BitPay, Inc. 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | -------------------------------------------------------------------------------- /test/typeforce.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | 3 | var typeforce = require('../lib/typeforce') 4 | 5 | describe('typeforce', function () { 6 | describe('enforceErrorSpec', function () { 7 | function getFn () { 8 | var args = arguments 9 | return function () { 10 | typeforce.enforceErrorSpec.apply(null, args) 11 | } 12 | } 13 | 14 | it('must be an object', function () { 15 | var spec = null 16 | expect(getFn(spec)).to.throw(TypeError, /Object/) 17 | }) 18 | 19 | it('name must be a string', function () { 20 | var spec = {name: 1} 21 | expect(getFn(spec)).to.throw(TypeError, /name/) 22 | }) 23 | 24 | it('name is not empty string', function () { 25 | var spec = {name: ''} 26 | expect(getFn(spec)).to.throw(TypeError, /empty string/) 27 | }) 28 | 29 | it('message are missing', function () { 30 | var spec = {name: 'custom'} 31 | expect(getFn(spec)).to.throw(TypeError, /message/) 32 | }) 33 | 34 | it('errors is undefined', function () { 35 | var spec = {name: 'custom', message: 'h1'} 36 | expect(getFn(spec)).to.not.throw(TypeError) 37 | }) 38 | 39 | it('errors not an array', function () { 40 | var spec = {name: 'custom', message: 'h1', errors: {}} 41 | expect(getFn(spec)).to.throw(TypeError, /errors/) 42 | }) 43 | 44 | it('recursive check', function () { 45 | var spec = {name: 'custom', message: 'h1', errors: [{name: ''}]} 46 | expect(getFn(spec)).to.throw(TypeError, /name/) 47 | }) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /lib/typeforce.js: -------------------------------------------------------------------------------- 1 | var util = require('./util') 2 | 3 | var getMsg = util.stringTemplate( 4 | 'Expected property `{name}` of type {expType}, got {gotType} for spec: {spec}') 5 | 6 | /** 7 | * @param {ErrorSpec} value 8 | * @throws {TypeError} 9 | */ 10 | module.exports.enforceErrorSpec = function (spec) { 11 | var msg 12 | 13 | if (util.getName(spec) !== 'Object') { 14 | throw new TypeError('spec must be an Object') 15 | } 16 | 17 | if (util.getName(spec.name) !== 'String') { 18 | msg = getMsg({ 19 | name: 'name', 20 | expType: 'String', 21 | gotType: util.getName(spec.name), 22 | spec: JSON.stringify(spec) 23 | }) 24 | throw new TypeError(msg) 25 | } 26 | 27 | if (spec.name === '') { 28 | msg = 'Expect "name" not empty string (spec: ' + JSON.stringify(spec) + ')' 29 | throw new TypeError(msg) 30 | } 31 | 32 | if (['String', 'Function'].indexOf(util.getName(spec.message)) === -1) { 33 | msg = getMsg({ 34 | name: 'message', 35 | expType: 'String or Function', 36 | gotType: util.getName(spec.message), 37 | spec: JSON.stringify(spec) 38 | }) 39 | throw new TypeError(msg) 40 | } 41 | 42 | if (spec.errors !== undefined) { 43 | if (util.getName(spec.errors) !== 'Array') { 44 | msg = getMsg({ 45 | name: 'errors', 46 | expType: 'Array', 47 | gotType: util.getName(spec.errors), 48 | spec: JSON.stringify(spec) 49 | }) 50 | throw new TypeError(msg) 51 | } 52 | 53 | spec.errors.forEach(module.exports.enforceErrorSpec) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/util.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | 3 | var util = require('../lib/util') 4 | 5 | describe('util', function () { 6 | describe('getName', function () { 7 | var objs = { 8 | 'Array': [], 9 | 'Function': function () {}, 10 | 'Null': null, 11 | 'Object': {}, 12 | 'String': '', 13 | 'Undefined': undefined 14 | } 15 | 16 | Object.keys(objs).forEach(function (key) { 17 | it(key, function () { 18 | expect(util.getName(objs[key])).to.equal(key) 19 | }) 20 | }) 21 | }) 22 | 23 | describe('stringTemplate', function () { 24 | it('Named arguments are replaced', function () { 25 | var result = util.stringTemplate('Hello {name}, how are you?')({ 26 | name: 'Mark' 27 | }) 28 | expect(result).to.equal('Hello Mark, how are you?') 29 | }) 30 | 31 | it('Named arguments can be escaped', function () { 32 | var result = util.stringTemplate('Hello {{name}}, how are you?')({ 33 | name: 'Mark' 34 | }) 35 | expect(result).to.equal('Hello {name}, how are you?') 36 | }) 37 | 38 | it('Array arguments are replaced', function () { 39 | var result = util.stringTemplate('Hello {0}, how are you?')(['Mark']) 40 | expect(result).to.equal('Hello Mark, how are you?') 41 | }) 42 | 43 | it('Template string without arguments', function () { 44 | var result = util.stringTemplate('Hello, how are you?')() 45 | expect(result).to.equal('Hello, how are you?') 46 | }) 47 | 48 | it('Not full escaped argument is latest', function () { 49 | var result = util.stringTemplate('Hello {{name}')({ 50 | name: 'Mark' 51 | }) 52 | expect(result).to.equal('Hello {Mark') 53 | }) 54 | 55 | it('Missing named arguments become 0 characters', function () { 56 | var result = util.stringTemplate('Hello{name}, how are you?')({}) 57 | expect(result).to.equal('Hello, how are you?') 58 | }) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "error-system", 3 | "version": "1.0.1", 4 | "description": "Your custom errors in your JavaScript code", 5 | "keywords": [ 6 | "error", 7 | "errors", 8 | "custom" 9 | ], 10 | "bugs": { 11 | "url": "https://github.com/fanatid/error-system/issues" 12 | }, 13 | "license": "MIT", 14 | "author": { 15 | "name": "Kirill Fomichev", 16 | "email": "fanatid@ya.ru" 17 | }, 18 | "files": [ 19 | "lib", 20 | "LICENSE", 21 | "README.md" 22 | ], 23 | "main": "lib/index.js", 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/fanatid/error-system.git" 27 | }, 28 | "scripts": { 29 | "compile": "browserify lib/index.js -s ErrorSystem -o error-system.js -g [ uglifyify --no-sourcemap ]", 30 | "compile:debug": "browserify lib/index.js -s ErrorSystem -o error-system.js -d", 31 | "coverage": "istanbul cover _mocha -- test/*.js", 32 | "coveralls": "npm run coverage && coveralls =0.10" 60 | }, 61 | "standard": { 62 | "ignore": [ 63 | "error-system.js" 64 | ], 65 | "globals": [ 66 | "describe", 67 | "it", 68 | "afterEach" 69 | ] 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {*} value 3 | * @return {string} 4 | */ 5 | module.exports.getName = function (value) { 6 | return Object.prototype.toString.call(value).slice(8, -1) 7 | } 8 | 9 | /** 10 | * @param {string} string 11 | * @return {function} 12 | */ 13 | module.exports.stringTemplate = function (string) { 14 | var nargs = /\{[0-9a-zA-Z]+\}/g 15 | var replacements = string.match(nargs) || [] 16 | var interleave = string.split(nargs) 17 | var replace = [] 18 | 19 | interleave.forEach(function (current, index) { 20 | replace.push({type: 'raw', value: current}) 21 | 22 | var replacement = replacements[index] 23 | if (replacement && replacement.length > 2) { 24 | var escapeLeft = current[current.length - 1] 25 | var escapeRight = (interleave[index + 1] || [])[0] 26 | replace.push({ 27 | type: escapeLeft === '{' && escapeRight === '}' ? 'raw' : 'tpl', 28 | value: replacement.slice(1, -1) 29 | }) 30 | } 31 | }) 32 | 33 | // join raw values 34 | replace = replace.reduce(function (result, current, index) { 35 | if (current.type === 'tpl' || result.length === 0) { 36 | result.push(current) 37 | } else { 38 | var prevIndex = result.length - 1 39 | if (result[prevIndex].type === 'raw') { 40 | result[prevIndex].value += current.value 41 | } else { 42 | result.push(current) 43 | } 44 | } 45 | 46 | return result 47 | }, []) 48 | 49 | return function () { 50 | var args = arguments.length === 1 && module.exports.getName(arguments[0]) === 'Object' 51 | ? arguments[0] 52 | : arguments 53 | 54 | // not map (arguments willl slow) 55 | var values = [] 56 | for (var idx = 0; idx < replace.length; ++idx) { 57 | var current = replace[idx] 58 | 59 | var value = '' 60 | if (current.type === 'raw') { 61 | value = current.value 62 | } else if (args[current.value] !== undefined) { 63 | value = args[current.value] 64 | } 65 | 66 | values.push(value) 67 | } 68 | 69 | return values.join('') 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var inherits = require('inherits') 2 | 3 | var typeforce = require('./typeforce') 4 | var util = require('./util') 5 | 6 | /** 7 | * @typedef {Object} ErrorSpec 8 | * @property {string} name 9 | * @property {(function|string)} message 10 | * @property {ErrorSpec[]} [errors] 11 | */ 12 | 13 | /** 14 | * @param {function} parent 15 | * @param {(ErrorSpec|ErrorSpec[])} specs 16 | */ 17 | module.exports.extend = function (parent, specs) { 18 | if (util.getName(parent) !== 'Function') { 19 | throw new TypeError('`parent` for extending should be a function') 20 | } 21 | 22 | if (util.getName(specs) !== 'Array') { 23 | specs = [specs] 24 | } 25 | 26 | // check all specs before extend, have overhead but all safe 27 | specs.forEach(function (spec) { 28 | typeforce.enforceErrorSpec(spec) 29 | }) 30 | 31 | specs.forEach(function (spec) { 32 | var getMessage = util.getName(spec.message) === 'String' 33 | ? util.stringTemplate(spec.message) 34 | : spec.message 35 | 36 | var CustomError = function () { 37 | parent.call(this) 38 | 39 | if (Error.captureStackTrace) { 40 | /* eslint-disable no-caller */ 41 | Error.captureStackTrace(this, arguments.callee) 42 | /* eslint-enable no-caller */ 43 | } else { 44 | this.stack = (new Error()).stack 45 | } 46 | 47 | this.message = getMessage.apply(null, arguments) 48 | } 49 | 50 | inherits(CustomError, parent) 51 | CustomError.prototype.name = parent.prototype.name + spec.name 52 | 53 | parent[spec.name] = CustomError 54 | 55 | if (spec.errors) { 56 | spec.errors.forEach(function (errorSpec) { 57 | module.exports.extend(CustomError, errorSpec) 58 | }) 59 | } 60 | }) 61 | 62 | return parent 63 | } 64 | 65 | /** 66 | * @param {string} name 67 | * @param {string} message 68 | * @param {function} [parent=Error] 69 | */ 70 | module.exports.createError = function (name, message, parent) { 71 | if (parent === undefined) { 72 | parent = Error 73 | } 74 | 75 | module.exports.extend(parent, {name: name, message: message}) 76 | return parent[name] 77 | } 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # error-system 2 | 3 | [![build status](https://img.shields.io/travis/fanatid/error-system.svg?branch=master&style=flat-square)](http://travis-ci.org/fanatid/error-system) 4 | [![Coverage Status](https://img.shields.io/coveralls/fanatid/error-system.svg?style=flat-square)](https://coveralls.io/r/fanatid/error-system) 5 | [![Dependency status](https://img.shields.io/david/fanatid/error-system.svg?style=flat-square)](https://david-dm.org/fanatid/error-system#info=dependencies) 6 | [![Dev Dependency status](https://img.shields.io/david/fanatid/error-system.svg?style=flat-square)](https://david-dm.org/fanatid/error-system#info=devDependencies) 7 | 8 | [![NPM](https://nodei.co/npm/error-system.png?downloads=true)](https://www.npmjs.com/package/error-system) 9 | [![js-standard-style](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard) 10 | 11 | Your custom errors in your JavaScript code! 12 | 13 | Inspired by [node-errno](http://github.com/rvagg/node-errno). 14 | 15 | Based on errors in [bitcore](https://github.com/bitpay/bitcore). 16 | 17 | ## Example 18 | 19 | ### createError 20 | ```js 21 | var errorSystem = require('error-system') 22 | var RequestError = errorSystem.createError('RequestError', 'Code: {0} (url: {1})') 23 | var request = require('request') 24 | 25 | var url = 'https://github.com/notfound11' 26 | request(url, function (error, response) { 27 | if (error === null && response.statusCode !== 200) { 28 | error = new RequestError(response.statusCode, url) 29 | } 30 | 31 | if (error !== null) { 32 | // ErrorRequestError: Code: 404 (url: https://github.com/notfound11) 33 | console.error(error.stack.split('\n')[0]) 34 | } 35 | }) 36 | ``` 37 | 38 | ### extend 39 | ```js 40 | var errorSystem = require('error-system') 41 | var RequestError = errorSystem.extend(Error, [{ 42 | name: 'RequestError', 43 | message: 'Code: {0} (url: {1})', 44 | errors: [ 45 | { 46 | name: 'NotFound', 47 | message: 'Code: 404 (url: {0})' 48 | } 49 | ] 50 | }]) 51 | var request = require('request') 52 | 53 | var url = 'https://github.com/notfound11' 54 | request(url, function (error, response) { 55 | if (error === null && response.statusCode !== 200) { 56 | if (response.statusCode === 404) { 57 | error = new RequestError.NotFound(url) 58 | } else if (response.statusCode !== 200) { 59 | error = new RequestError(response.statusCode, url) 60 | } 61 | } 62 | 63 | if (error !== null) { 64 | // ErrorRequestErrorNotFound: Code: 404 (url: https://github.com/notfound11) 65 | console.error(error.stack.split('\n')[0]) 66 | } 67 | }) 68 | ``` 69 | 70 | ## License 71 | 72 | Code released under [the MIT license](https://github.com/fanatid/error-system/blob/master/LICENSE). 73 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | var errorSystem = require('../lib') 3 | 4 | var extend = errorSystem.extend 5 | var createError = errorSystem.createError 6 | 7 | function testCustomError (PError, cName, cArgs, cMessage) { 8 | var CError = PError[cName] 9 | expect(CError).to.be.a('Function') 10 | var CErrorConstructor = Function.prototype.bind.apply(CError, [null].concat(cArgs)) 11 | var custom = new CErrorConstructor() 12 | expect(custom).to.be.instanceof(PError) 13 | expect(custom).to.be.instanceof(CError) 14 | expect(custom.message).to.equal(cMessage) 15 | } 16 | 17 | describe('extend', function () { 18 | it('invalid parent', function () { 19 | function fn () { extend('') } 20 | expect(fn).to.throw(TypeError, /parent/) 21 | }) 22 | 23 | it('invalid spec', function () { 24 | function fn () { extend(Error, '') } 25 | expect(fn).to.throw(TypeError, /Object/) 26 | }) 27 | 28 | it('spec.message a function', function () { 29 | function getMessage () { 30 | return 'arguments: ' + Array.prototype.join.call(arguments, ',') 31 | } 32 | extend(Error, [{name: 'Custom2', message: getMessage}]) 33 | testCustomError(Error, 'Custom2', ['a', 'b'], 'arguments: a,b') 34 | }) 35 | 36 | it('spec.message a string', function () { 37 | extend(Error, {name: 'Custom3', message: 'arguments: {0},{1},{2}'}) 38 | testCustomError(Error, 'Custom3', ['a', 'b'], 'arguments: a,b,') 39 | }) 40 | 41 | it('recursive extend via errors', function () { 42 | var spec = { 43 | name: 'Custom4', 44 | message: 'Custom4: 123', 45 | errors: [ 46 | { 47 | name: 'Custom5', 48 | message: 'Custom5: 234', 49 | errors: [ 50 | { 51 | name: 'Custom6', 52 | message: 'Custom6: 345' 53 | } 54 | ] 55 | } 56 | ] 57 | } 58 | 59 | extend(Error, spec) 60 | testCustomError(Error, 'Custom4', [], 'Custom4: 123') 61 | testCustomError(Error.Custom4, 'Custom5', [], 'Custom5: 234') 62 | testCustomError(Error.Custom4.Custom5, 'Custom6', [], 'Custom6: 345') 63 | }) 64 | 65 | describe('stack property', function () { 66 | var captureStackTrace = Error.captureStackTrace 67 | 68 | afterEach(function () { 69 | Error.captureStackTrace = captureStackTrace 70 | }) 71 | 72 | it('use (new Error()).stack for stack', function () { 73 | delete Error.captureStackTrace 74 | 75 | var Custom21 = createError('Custom21', '{0}', Error) 76 | var custom21 = new Custom21() 77 | expect(custom21.stack).to.match(/CustomError/) 78 | }) 79 | 80 | it('use captureStackTrace for stack', function () { 81 | if (Error.captureStackTrace === undefined) { 82 | return 83 | } 84 | 85 | var Custom22 = createError('Custom22', '{0}', Error) 86 | var custom22 = new Custom22() 87 | expect(custom22.stack.split('\n')[0]).to.equal('ErrorCustom22') 88 | expect(custom22.stack.split('\n')[1]).to.match(/Context\./) 89 | }) 90 | }) 91 | }) 92 | 93 | describe('createError', function () { 94 | it('with default parent (as Error)', function () { 95 | createError('Custom10', 'Custom10: {0}') 96 | testCustomError(Error, 'Custom10', ['h1'], 'Custom10: h1') 97 | }) 98 | 99 | it('with named error', function () { 100 | createError('Custom10', 'Custom10: Hi {name}!') 101 | testCustomError(Error, 'Custom10', {name: 'Mark'}, 'Custom10: Hi Mark!') 102 | }) 103 | 104 | it('with custom parent', function () { 105 | function OtherError () {} 106 | createError('Custom11', 'Custom11: {0}', OtherError) 107 | testCustomError(OtherError, 'Custom11', ['h1'], 'Custom11: h1') 108 | }) 109 | }) 110 | --------------------------------------------------------------------------------