├── .editorconfig ├── .gitignore ├── .istanbul.yml ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── package.json └── test └── test.coffee /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.coffee] 11 | indent_style = space 12 | 13 | [{package.json,*.yml}] 14 | indent_style = space 15 | indent_size = 2 16 | 17 | [*.md] 18 | trim_trailing_whitespace = false 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sw[a-p] 2 | /node_modules/ 3 | npm-debug.log 4 | /coverage/ 5 | yarn.lock 6 | yarn-error.log 7 | -------------------------------------------------------------------------------- /.istanbul.yml: -------------------------------------------------------------------------------- 1 | instrumentation: 2 | excludes: 3 | - test.js 4 | - test/**/* 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | script: 4 | - node_modules/.bin/istanbul cover node_modules/.bin/_mocha -- --compilers coffee:coffee-script/register 5 | - cat coverage/lcov.info | node_modules/.bin/coveralls 6 | node_js: 7 | - "0.10" 8 | - "0.11" 9 | - "0.12" 10 | - "iojs" 11 | - "4" 12 | - "6" 13 | - "7" 14 | - "stable" 15 | os: 16 | - linux 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 JD Ballard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-error-ex [![Travis-CI.org Build Status](https://img.shields.io/travis/Qix-/node-error-ex.svg?style=flat-square)](https://travis-ci.org/Qix-/node-error-ex) [![Coveralls.io Coverage Rating](https://img.shields.io/coveralls/Qix-/node-error-ex.svg?style=flat-square)](https://coveralls.io/r/Qix-/node-error-ex) 2 | > Easily subclass and customize new Error types 3 | 4 | ## Examples 5 | To include in your project: 6 | ```javascript 7 | var errorEx = require('error-ex'); 8 | ``` 9 | 10 | To create an error message type with a specific name (note, that `ErrorFn.name` 11 | will not reflect this): 12 | ```javascript 13 | var JSONError = errorEx('JSONError'); 14 | 15 | var err = new JSONError('error'); 16 | err.name; //-> JSONError 17 | throw err; //-> JSONError: error 18 | ``` 19 | 20 | To add a stack line: 21 | ```javascript 22 | var JSONError = errorEx('JSONError', {fileName: errorEx.line('in %s')}); 23 | 24 | var err = new JSONError('error') 25 | err.fileName = '/a/b/c/foo.json'; 26 | throw err; //-> (line 2)-> in /a/b/c/foo.json 27 | ``` 28 | 29 | To append to the error message: 30 | ```javascript 31 | var JSONError = errorEx('JSONError', {fileName: errorEx.append('in %s')}); 32 | 33 | var err = new JSONError('error'); 34 | err.fileName = '/a/b/c/foo.json'; 35 | throw err; //-> JSONError: error in /a/b/c/foo.json 36 | ``` 37 | 38 | ## API 39 | 40 | #### `errorEx([name], [properties])` 41 | Creates a new ErrorEx error type 42 | 43 | - `name`: the name of the new type (appears in the error message upon throw; 44 | defaults to `Error.name`) 45 | - `properties`: if supplied, used as a key/value dictionary of properties to 46 | use when building up the stack message. Keys are property names that are 47 | looked up on the error message, and then passed to function values. 48 | - `line`: if specified and is a function, return value is added as a stack 49 | entry (error-ex will indent for you). Passed the property value given 50 | the key. 51 | - `stack`: if specified and is a function, passed the value of the property 52 | using the key, and the raw stack lines as a second argument. Takes no 53 | return value (but the stack can be modified directly). 54 | - `message`: if specified and is a function, return value is used as new 55 | `.message` value upon get. Passed the property value of the property named 56 | by key, and the existing message is passed as the second argument as an 57 | array of lines (suitable for multi-line messages). 58 | 59 | Returns a constructor (Function) that can be used just like the regular Error 60 | constructor. 61 | 62 | ```javascript 63 | var errorEx = require('error-ex'); 64 | 65 | var BasicError = errorEx(); 66 | 67 | var NamedError = errorEx('NamedError'); 68 | 69 | // -- 70 | 71 | var AdvancedError = errorEx('AdvancedError', { 72 | foo: { 73 | line: function (value, stack) { 74 | if (value) { 75 | return 'bar ' + value; 76 | } 77 | return null; 78 | } 79 | } 80 | }) 81 | 82 | var err = new AdvancedError('hello, world'); 83 | err.foo = 'baz'; 84 | throw err; 85 | 86 | /* 87 | AdvancedError: hello, world 88 | bar baz 89 | at tryReadme() (readme.js:20:1) 90 | */ 91 | ``` 92 | 93 | #### `errorEx.line(str)` 94 | Creates a stack line using a delimiter 95 | 96 | > This is a helper function. It is to be used in lieu of writing a value object 97 | > for `properties` values. 98 | 99 | - `str`: The string to create 100 | - Use the delimiter `%s` to specify where in the string the value should go 101 | 102 | ```javascript 103 | var errorEx = require('error-ex'); 104 | 105 | var FileError = errorEx('FileError', {fileName: errorEx.line('in %s')}); 106 | 107 | var err = new FileError('problem reading file'); 108 | err.fileName = '/a/b/c/d/foo.js'; 109 | throw err; 110 | 111 | /* 112 | FileError: problem reading file 113 | in /a/b/c/d/foo.js 114 | at tryReadme() (readme.js:7:1) 115 | */ 116 | ``` 117 | 118 | #### `errorEx.append(str)` 119 | Appends to the `error.message` string 120 | 121 | > This is a helper function. It is to be used in lieu of writing a value object 122 | > for `properties` values. 123 | 124 | - `str`: The string to append 125 | - Use the delimiter `%s` to specify where in the string the value should go 126 | 127 | ```javascript 128 | var errorEx = require('error-ex'); 129 | 130 | var SyntaxError = errorEx('SyntaxError', {fileName: errorEx.append('in %s')}); 131 | 132 | var err = new SyntaxError('improper indentation'); 133 | err.fileName = '/a/b/c/d/foo.js'; 134 | throw err; 135 | 136 | /* 137 | SyntaxError: improper indentation in /a/b/c/d/foo.js 138 | at tryReadme() (readme.js:7:1) 139 | */ 140 | ``` 141 | 142 | ## License 143 | Licensed under the [MIT License](http://opensource.org/licenses/MIT). 144 | You can find a copy of it in [LICENSE](LICENSE). 145 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('util'); 4 | var isArrayish = require('is-arrayish'); 5 | 6 | var errorEx = function errorEx(name, properties) { 7 | if (!name || name.constructor !== String) { 8 | properties = name || {}; 9 | name = Error.name; 10 | } 11 | 12 | var errorExError = function ErrorEXError(message) { 13 | if (!this) { 14 | return new ErrorEXError(message); 15 | } 16 | 17 | message = message instanceof Error 18 | ? message.message 19 | : (message || this.message); 20 | 21 | Error.call(this, message); 22 | Error.captureStackTrace(this, errorExError); 23 | 24 | this.name = name; 25 | 26 | Object.defineProperty(this, 'message', { 27 | configurable: true, 28 | enumerable: false, 29 | get: function () { 30 | var newMessage = message.split(/\r?\n/g); 31 | 32 | for (var key in properties) { 33 | if (!properties.hasOwnProperty(key)) { 34 | continue; 35 | } 36 | 37 | var modifier = properties[key]; 38 | 39 | if ('message' in modifier) { 40 | newMessage = modifier.message(this[key], newMessage) || newMessage; 41 | if (!isArrayish(newMessage)) { 42 | newMessage = [newMessage]; 43 | } 44 | } 45 | } 46 | 47 | return newMessage.join('\n'); 48 | }, 49 | set: function (v) { 50 | message = v; 51 | } 52 | }); 53 | 54 | var overwrittenStack = null; 55 | 56 | var stackDescriptor = Object.getOwnPropertyDescriptor(this, 'stack'); 57 | var stackGetter = stackDescriptor.get; 58 | var stackValue = stackDescriptor.value; 59 | delete stackDescriptor.value; 60 | delete stackDescriptor.writable; 61 | 62 | stackDescriptor.set = function (newstack) { 63 | overwrittenStack = newstack; 64 | }; 65 | 66 | stackDescriptor.get = function () { 67 | var stack = (overwrittenStack || ((stackGetter) 68 | ? stackGetter.call(this) 69 | : stackValue)).split(/\r?\n+/g); 70 | 71 | // starting in Node 7, the stack builder caches the message. 72 | // just replace it. 73 | if (!overwrittenStack) { 74 | stack[0] = this.name + ': ' + this.message; 75 | } 76 | 77 | var lineCount = 1; 78 | for (var key in properties) { 79 | if (!properties.hasOwnProperty(key)) { 80 | continue; 81 | } 82 | 83 | var modifier = properties[key]; 84 | 85 | if ('line' in modifier) { 86 | var line = modifier.line(this[key]); 87 | if (line) { 88 | stack.splice(lineCount++, 0, ' ' + line); 89 | } 90 | } 91 | 92 | if ('stack' in modifier) { 93 | modifier.stack(this[key], stack); 94 | } 95 | } 96 | 97 | return stack.join('\n'); 98 | }; 99 | 100 | Object.defineProperty(this, 'stack', stackDescriptor); 101 | }; 102 | 103 | if (Object.setPrototypeOf) { 104 | Object.setPrototypeOf(errorExError.prototype, Error.prototype); 105 | Object.setPrototypeOf(errorExError, Error); 106 | } else { 107 | util.inherits(errorExError, Error); 108 | } 109 | 110 | return errorExError; 111 | }; 112 | 113 | errorEx.append = function (str, def) { 114 | return { 115 | message: function (v, message) { 116 | v = v || def; 117 | 118 | if (v) { 119 | message[0] += ' ' + str.replace('%s', v.toString()); 120 | } 121 | 122 | return message; 123 | } 124 | }; 125 | }; 126 | 127 | errorEx.line = function (str, def) { 128 | return { 129 | line: function (v) { 130 | v = v || def; 131 | 132 | if (v) { 133 | return str.replace('%s', v.toString()); 134 | } 135 | 136 | return null; 137 | } 138 | }; 139 | }; 140 | 141 | module.exports = errorEx; 142 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "error-ex", 3 | "description": "Easy error subclassing and stack customization", 4 | "version": "1.3.2", 5 | "maintainers": [ 6 | "Josh Junon (github.com/qix-)", 7 | "Sindre Sorhus (sindresorhus.com)" 8 | ], 9 | "keywords": [ 10 | "error", 11 | "errors", 12 | "extend", 13 | "extending", 14 | "extension", 15 | "subclass", 16 | "stack", 17 | "custom" 18 | ], 19 | "license": "MIT", 20 | "scripts": { 21 | "pretest": "xo", 22 | "test": "mocha --compilers coffee:coffee-script/register" 23 | }, 24 | "xo": { 25 | "rules": { 26 | "operator-linebreak": [ 27 | 0 28 | ] 29 | } 30 | }, 31 | "repository": "qix-/node-error-ex", 32 | "files": [ 33 | "index.js" 34 | ], 35 | "devDependencies": { 36 | "coffee-script": "^1.9.3", 37 | "coveralls": "^2.11.2", 38 | "istanbul": "^0.3.17", 39 | "mocha": "^2.2.5", 40 | "should": "^7.0.1", 41 | "xo": "^0.7.1" 42 | }, 43 | "dependencies": { 44 | "is-arrayish": "^0.2.1" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/test.coffee: -------------------------------------------------------------------------------- 1 | should = require 'should' 2 | errorEx = require '../' 3 | 4 | Error.stackTraceLimit = Infinity 5 | 6 | it 'should create a default error type', -> 7 | TestError = errorEx() 8 | err = new TestError 'herp derp' 9 | err.should.be.instanceOf TestError 10 | err.should.be.instanceOf Error 11 | err.name.should.equal Error.name 12 | err.message.should.equal 'herp derp' 13 | 14 | it 'should create a new error type', -> 15 | TestError = errorEx 'TestError' 16 | err = new TestError 'herp derp' 17 | err.should.be.instanceOf TestError 18 | err.should.be.instanceOf Error 19 | err.name.should.equal 'TestError' 20 | testLine = err.stack.toString().split(/\r?\n/g)[0] 21 | testLine.should.equal 'TestError: herp derp' 22 | 23 | it 'should add a custom property line', -> 24 | TestError = errorEx 'TestError', foo:line: -> 'bar' 25 | err = new TestError 'herp derp' 26 | testLine = err.stack.toString().split(/\r?\n/g)[1] 27 | testLine.should.equal ' bar' 28 | 29 | it 'should allow properties', -> 30 | TestError = errorEx 'TestError', foo:line: (v)-> "foo #{v}" if v 31 | err = new TestError 'herp derp' 32 | testLine = err.stack.toString().split(/\r?\n/g)[1] 33 | testLine.substr(0, 3).should.not.equal 'foo' 34 | err.foo = 'bar' 35 | testLine = err.stack.toString().split(/\r?\n/g)[1] 36 | testLine.should.equal ' foo bar' 37 | 38 | it 'should allow direct editing of the stack', -> 39 | TestError = errorEx 'TestError', 40 | foo:stack: (v, stack)-> stack[0] += " #{v}" if v 41 | err = new TestError 'herp derp' 42 | err.foo = 'magerp' 43 | testLine = err.stack.toString().split(/\r?\n/g)[0] 44 | testLine.should.equal 'TestError: herp derp magerp' 45 | 46 | it 'should work on existing errors', -> 47 | originalErr = new Error 'herp derp' 48 | TestError = errorEx 'TestError', foo:line: (v)-> "foo #{v}" 49 | TestError.call originalErr 50 | originalErr.message.should.equal 'herp derp' 51 | originalErr.name.should.equal 'TestError' 52 | originalErr.foo = 'bar' 53 | testLine = originalErr.stack.toString().split(/\r?\n/g)[1] 54 | testLine.should.equal ' foo bar' 55 | 56 | it 'should take in an existing error to the constructor', -> 57 | originalErr = new Error 'herp derp' 58 | TestError = errorEx 'TestError' 59 | newErr = new TestError originalErr 60 | newErr.message.should.equal originalErr.message 61 | 62 | it 'should allow the editing of the message', -> 63 | originalErr = new Error 'herp derp' 64 | TestError = errorEx 'TestError', foo:message: ()-> 'foobar' 65 | TestError.call originalErr 66 | originalErr.message.should.equal 'foobar' 67 | 68 | it 'should allow the editing of the message (with value)', -> 69 | originalErr = new Error 'herp derp' 70 | TestError = errorEx 'TestError', foo:message: (v)-> "foobar #{v}" 71 | TestError.call originalErr 72 | originalErr.foo = '1234' 73 | originalErr.message.should.equal 'foobar 1234' 74 | 75 | it 'should allow the editing of the message (multiple lines)', -> 76 | originalErr = new Error 'herp derp' 77 | TestError = errorEx 'TestError', foo:message: (v)-> ['hello', "foobar #{v}"] 78 | TestError.call originalErr 79 | originalErr.foo = '1234' 80 | originalErr.message.should.equal 'hello\nfoobar 1234' 81 | 82 | it 'should allow the editing of the message (append original)', -> 83 | originalErr = new Error 'herp derp' 84 | TestError = errorEx 'TestError', foo:message: (v, message)-> message.concat ['hello, there'] 85 | TestError.call originalErr 86 | originalErr.message.should.equal 'herp derp\nhello, there' 87 | 88 | describe 'helpers', -> 89 | describe 'append', -> 90 | it 'should append to the error string', -> 91 | TestError = errorEx 'TestError', fileName: errorEx.append 'in %s' 92 | err = new TestError 'error' 93 | err.fileName = '/a/b/c/foo.txt' 94 | testLine = err.stack.toString().split(/\r?\n/g)[0] 95 | testLine.should.equal 'TestError: error in /a/b/c/foo.txt' 96 | 97 | it 'should append to a multi-line error string', -> 98 | TestError = errorEx 'TestError', fileName: errorEx.append 'in %s' 99 | err = new TestError 'error\n}\n^' 100 | err.fileName = '/a/b/c/foo.txt' 101 | testLine = err.stack.toString().split(/\r?\n/g)[0] 102 | testLine.should.equal 'TestError: error in /a/b/c/foo.txt' 103 | err.message.should.equal 'error in /a/b/c/foo.txt\n}\n^' 104 | 105 | it 'should append and use toString()', -> 106 | TestError = errorEx 'TestError', fileName: errorEx.append 'in %s' 107 | err = new TestError 'error' 108 | err.fileName = '/a/b/c/foo.txt' 109 | err.toString().should.equal 'TestError: error in /a/b/c/foo.txt' 110 | err.message.should.equal 'error in /a/b/c/foo.txt' 111 | 112 | it 'should append and use toString() on existing error', -> 113 | TestError = errorEx 'TestError', fileName: errorEx.append 'in %s' 114 | err = new Error 'error' 115 | TestError.call err 116 | err.fileName = '/a/b/c/foo.txt' 117 | err.toString().should.equal 'TestError: error in /a/b/c/foo.txt' 118 | err.message.should.equal 'error in /a/b/c/foo.txt' 119 | 120 | describe 'line', -> 121 | it 'should create a new line', -> 122 | TestError = errorEx 'TestError', fileName: errorEx.line 'in %s' 123 | err = new TestError 'error' 124 | err.fileName = '/a/b/c/foo.txt' 125 | testLine = err.stack.toString().split(/\r?\n/g)[1] 126 | testLine.should.equal ' in /a/b/c/foo.txt' 127 | 128 | describe 'bluebird support', -> 129 | it 'should pass the bluebird stack write test', -> 130 | bluebirdPropertyWritable = (obj, prop)-> 131 | descriptor = Object.getOwnPropertyDescriptor(obj, prop) 132 | return !!(!descriptor || descriptor.writable || descriptor.set) 133 | 134 | TestError = errorEx 'TestError', fileName: errorEx.line 'in %s' 135 | err = new TestError 'error' 136 | err_native = new Error 'hello' 137 | 138 | (bluebirdPropertyWritable err_native, 'stack').should.be.ok() 139 | (bluebirdPropertyWritable err, 'stack').should.be.ok() 140 | 141 | it 'should allow the stack to be set while preserving custom properties', -> 142 | TestError = errorEx 'TestError', fileName: errorEx.line 'in %s' 143 | 144 | err = new TestError 'error' 145 | err.fileName = '/a/b/c/foo.txt' 146 | testLine = err.stack.toString().split(/\r?\n/g)[1] 147 | testLine.should.equal ' in /a/b/c/foo.txt' 148 | 149 | err.stack = 'TestError: overridden error\n at null:1:1' 150 | err.stack.toString().should.equal 'TestError: overridden error\n in /a/b/c/foo.txt\n at null:1:1' 151 | 152 | err.stack = null 153 | testLine = err.stack.toString().split(/\r?\n/g)[1] 154 | testLine.should.equal ' in /a/b/c/foo.txt' 155 | --------------------------------------------------------------------------------