-
Notifications
You must be signed in to change notification settings - Fork 30k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add the internal/errors.js core mechanism. PR-URL: #11220 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Joyee Cheung <[email protected]>
- Loading branch information
1 parent
a710167
commit 214a392
Showing
5 changed files
with
361 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
# Using the internal/errors.js Module | ||
|
||
## What is internal/errors.js | ||
|
||
The `require('internal/errors')` module is an internal-only module that can be | ||
used to produce `Error`, `TypeError` and `RangeError` instances that use a | ||
static, permanent error code and an optionally parameterized message. | ||
|
||
The intent of the module is to allow errors provided by Node.js to be assigned a | ||
permanent identifier. Without a permanent identifier, userland code may need to | ||
inspect error messages to distinguish one error from another. An unfortunate | ||
result of that practice is that changes to error messages result in broken code | ||
in the ecosystem. For that reason, Node.js has considered error message changes | ||
to be breaking changes. By providing a permanent identifier for a specific | ||
error, we reduce the need for userland code to inspect error messages. | ||
|
||
*Note*: Switching an existing error to use the `internal/errors` module must be | ||
considered a `semver-major` change. However, once using `internal/errors`, | ||
changes to `internal/errors` error messages will be handled as `semver-minor` | ||
or `semver-patch`. | ||
|
||
## Using internal/errors.js | ||
|
||
The `internal/errors` module exposes three custom `Error` classes that | ||
are intended to replace existing `Error` objects within the Node.js source. | ||
|
||
For instance, an existing `Error` such as: | ||
|
||
```js | ||
var err = new TypeError('Expected string received ' + type); | ||
``` | ||
|
||
Can be replaced by first adding a new error key into the `internal/errors.js` | ||
file: | ||
|
||
```js | ||
E('FOO', 'Expected string received %s'); | ||
``` | ||
|
||
Then replacing the existing `new TypeError` in the code: | ||
|
||
```js | ||
const errors = require('internal/errors'); | ||
// ... | ||
var err = new errors.TypeError('FOO', type); | ||
``` | ||
|
||
## Adding new errors | ||
|
||
New static error codes are added by modifying the `internal/errors.js` file | ||
and appending the new error codes to the end using the utility `E()` method. | ||
|
||
```js | ||
E('EXAMPLE_KEY1', 'This is the error value'); | ||
E('EXAMPLE_KEY2', (a, b) => return `${a} ${b}`); | ||
``` | ||
|
||
The first argument passed to `E()` is the static identifier. The second | ||
argument is either a String with optional `util.format()` style replacement | ||
tags (e.g. `%s`, `%d`), or a function returning a String. The optional | ||
additional arguments passed to the `errors.message()` function (which is | ||
used by the `errors.Error`, `errors.TypeError` and `errors.RangeError` classes), | ||
will be used to format the error message. | ||
|
||
## Documenting new errors | ||
|
||
Whenever a new static error code is added and used, corresponding documentation | ||
for the error code should be added to the `doc/api/errors.md` file. This will | ||
give users a place to go to easily look up the meaning of individual error | ||
codes. | ||
|
||
|
||
## API | ||
|
||
### Class: errors.Error(key[, args...]) | ||
|
||
* `key` {String} The static error identifier | ||
* `args...` {Any} Zero or more optional arguments | ||
|
||
```js | ||
const errors = require('internal/errors'); | ||
|
||
var arg1 = 'foo'; | ||
var arg2 = 'bar'; | ||
const myError = new errors.Error('KEY', arg1, arg2); | ||
throw myError; | ||
``` | ||
|
||
The specific error message for the `myError` instance will depend on the | ||
associated value of `KEY` (see "Adding new errors"). | ||
|
||
The `myError` object will have a `code` property equal to the `key` and a | ||
`name` property equal to `Error[${key}]`. | ||
|
||
### Class: errors.TypeError(key[, args...]) | ||
|
||
* `key` {String} The static error identifier | ||
* `args...` {Any} Zero or more optional arguments | ||
|
||
```js | ||
const errors = require('internal/errors'); | ||
|
||
var arg1 = 'foo'; | ||
var arg2 = 'bar'; | ||
const myError = new errors.TypeError('KEY', arg1, arg2); | ||
throw myError; | ||
``` | ||
|
||
The specific error message for the `myError` instance will depend on the | ||
associated value of `KEY` (see "Adding new errors"). | ||
|
||
The `myError` object will have a `code` property equal to the `key` and a | ||
`name` property equal to `TypeError[${key}]`. | ||
|
||
### Class: errors.RangeError(key[, args...]) | ||
|
||
* `key` {String} The static error identifier | ||
* `args...` {Any} Zero or more optional arguments | ||
|
||
```js | ||
const errors = require('internal/errors'); | ||
|
||
var arg1 = 'foo'; | ||
var arg2 = 'bar'; | ||
const myError = new errors.RangeError('KEY', arg1, arg2); | ||
throw myError; | ||
``` | ||
|
||
The specific error message for the `myError` instance will depend on the | ||
associated value of `KEY` (see "Adding new errors"). | ||
|
||
The `myError` object will have a `code` property equal to the `key` and a | ||
`name` property equal to `RangeError[${key}]`. | ||
|
||
### Method: errors.message(key, args) | ||
|
||
* `key` {String} The static error identifier | ||
* `args` {Array} Zero or more optional arguments passed as an Array | ||
* Returns: {String} | ||
|
||
Returns the formatted error message string for the given `key`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
'use strict'; | ||
|
||
// The whole point behind this internal module is to allow Node.js to no | ||
// longer be forced to treat every error message change as a semver-major | ||
// change. The NodeError classes here all expose a `code` property whose | ||
// value statically and permanently identifies the error. While the error | ||
// message may change, the code should not. | ||
|
||
const kCode = Symbol('code'); | ||
const messages = new Map(); | ||
|
||
var assert, util; | ||
function lazyAssert() { | ||
if (!assert) | ||
assert = require('assert'); | ||
return assert; | ||
} | ||
|
||
function lazyUtil() { | ||
if (!util) | ||
util = require('util'); | ||
return util; | ||
} | ||
|
||
function makeNodeError(Base) { | ||
return class NodeError extends Base { | ||
constructor(key, ...args) { | ||
super(message(key, args)); | ||
this[kCode] = key; | ||
Error.captureStackTrace(this, NodeError); | ||
} | ||
|
||
get name() { | ||
return `${super.name}[${this[kCode]}]`; | ||
} | ||
|
||
get code() { | ||
return this[kCode]; | ||
} | ||
}; | ||
} | ||
|
||
function message(key, args) { | ||
const assert = lazyAssert(); | ||
assert.strictEqual(typeof key, 'string'); | ||
const util = lazyUtil(); | ||
const msg = messages.get(key); | ||
assert(msg, `An invalid error message key was used: ${key}.`); | ||
let fmt = util.format; | ||
if (typeof msg === 'function') { | ||
fmt = msg; | ||
} else { | ||
if (args === undefined || args.length === 0) | ||
return msg; | ||
args.unshift(msg); | ||
} | ||
return String(fmt.apply(null, args)); | ||
} | ||
|
||
// Utility function for registering the error codes. Only used here. Exported | ||
// *only* to allow for testing. | ||
function E(sym, val) { | ||
messages.set(sym, typeof val === 'function' ? val : String(val)); | ||
} | ||
|
||
module.exports = exports = { | ||
message, | ||
Error: makeNodeError(Error), | ||
TypeError: makeNodeError(TypeError), | ||
RangeError: makeNodeError(RangeError), | ||
E // This is exported only to facilitate testing. | ||
}; | ||
|
||
// To declare an error message, use the E(sym, val) function above. The sym | ||
// must be an upper case string. The val can be either a function or a string. | ||
// The return value of the function must be a string. | ||
// Examples: | ||
// E('EXAMPLE_KEY1', 'This is the error value'); | ||
// E('EXAMPLE_KEY2', (a, b) => return `${a} ${b}`); | ||
// | ||
// Once an error code has been assigned, the code itself MUST NOT change and | ||
// any given error code must never be reused to identify a different error. | ||
// | ||
// Any error code added here should also be added to the documentation | ||
// | ||
// Note: Please try to keep these in alphabetical order | ||
E('ERR_ASSERTION', (msg) => msg); | ||
// Add new errors from here... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
// Flags: --expose-internals | ||
'use strict'; | ||
|
||
const common = require('../common'); | ||
const errors = require('internal/errors'); | ||
const assert = require('assert'); | ||
|
||
errors.E('TEST_ERROR_1', 'Error for testing purposes: %s'); | ||
errors.E('TEST_ERROR_2', (a, b) => `${a} ${b}`); | ||
|
||
const err1 = new errors.Error('TEST_ERROR_1', 'test'); | ||
const err2 = new errors.TypeError('TEST_ERROR_1', 'test'); | ||
const err3 = new errors.RangeError('TEST_ERROR_1', 'test'); | ||
const err4 = new errors.Error('TEST_ERROR_2', 'abc', 'xyz'); | ||
|
||
assert(err1 instanceof Error); | ||
assert.strictEqual(err1.name, 'Error[TEST_ERROR_1]'); | ||
assert.strictEqual(err1.message, 'Error for testing purposes: test'); | ||
assert.strictEqual(err1.code, 'TEST_ERROR_1'); | ||
|
||
assert(err2 instanceof TypeError); | ||
assert.strictEqual(err2.name, 'TypeError[TEST_ERROR_1]'); | ||
assert.strictEqual(err2.message, 'Error for testing purposes: test'); | ||
assert.strictEqual(err2.code, 'TEST_ERROR_1'); | ||
|
||
assert(err3 instanceof RangeError); | ||
assert.strictEqual(err3.name, 'RangeError[TEST_ERROR_1]'); | ||
assert.strictEqual(err3.message, 'Error for testing purposes: test'); | ||
assert.strictEqual(err3.code, 'TEST_ERROR_1'); | ||
|
||
assert(err4 instanceof Error); | ||
assert.strictEqual(err4.name, 'Error[TEST_ERROR_2]'); | ||
assert.strictEqual(err4.message, 'abc xyz'); | ||
assert.strictEqual(err4.code, 'TEST_ERROR_2'); | ||
|
||
assert.throws( | ||
() => new errors.Error('TEST_FOO_KEY'), | ||
/^AssertionError: An invalid error message key was used: TEST_FOO_KEY.$/); | ||
// Calling it twice yields same result (using the key does not create it) | ||
assert.throws( | ||
() => new errors.Error('TEST_FOO_KEY'), | ||
/^AssertionError: An invalid error message key was used: TEST_FOO_KEY.$/); | ||
assert.throws( | ||
() => new errors.Error(1), | ||
/^AssertionError: 'number' === 'string'$/); | ||
assert.throws( | ||
() => new errors.Error({}), | ||
/^AssertionError: 'object' === 'string'$/); | ||
assert.throws( | ||
() => new errors.Error([]), | ||
/^AssertionError: 'object' === 'string'$/); | ||
assert.throws( | ||
() => new errors.Error(true), | ||
/^AssertionError: 'boolean' === 'string'$/); | ||
assert.throws( | ||
() => new errors.TypeError(1), | ||
/^AssertionError: 'number' === 'string'$/); | ||
assert.throws( | ||
() => new errors.TypeError({}), | ||
/^AssertionError: 'object' === 'string'$/); | ||
assert.throws( | ||
() => new errors.TypeError([]), | ||
/^AssertionError: 'object' === 'string'$/); | ||
assert.throws( | ||
() => new errors.TypeError(true), | ||
/^AssertionError: 'boolean' === 'string'$/); | ||
assert.throws( | ||
() => new errors.RangeError(1), | ||
/^AssertionError: 'number' === 'string'$/); | ||
assert.throws( | ||
() => new errors.RangeError({}), | ||
/^AssertionError: 'object' === 'string'$/); | ||
assert.throws( | ||
() => new errors.RangeError([]), | ||
/^AssertionError: 'object' === 'string'$/); | ||
assert.throws( | ||
() => new errors.RangeError(true), | ||
/^AssertionError: 'boolean' === 'string'$/); | ||
|
||
|
||
// Tests for common.expectsError | ||
assert.doesNotThrow(() => { | ||
assert.throws(() => { | ||
throw new errors.TypeError('TEST_ERROR_1', 'a'); | ||
}, common.expectsError('TEST_ERROR_1')); | ||
}); | ||
|
||
assert.doesNotThrow(() => { | ||
assert.throws(() => { | ||
throw new errors.TypeError('TEST_ERROR_1', 'a'); | ||
}, common.expectsError('TEST_ERROR_1', TypeError, /^Error for testing/)); | ||
}); | ||
|
||
assert.doesNotThrow(() => { | ||
assert.throws(() => { | ||
throw new errors.TypeError('TEST_ERROR_1', 'a'); | ||
}, common.expectsError('TEST_ERROR_1', TypeError)); | ||
}); | ||
|
||
assert.doesNotThrow(() => { | ||
assert.throws(() => { | ||
throw new errors.TypeError('TEST_ERROR_1', 'a'); | ||
}, common.expectsError('TEST_ERROR_1', Error)); | ||
}); | ||
|
||
assert.throws(() => { | ||
assert.throws(() => { | ||
throw new errors.TypeError('TEST_ERROR_1', 'a'); | ||
}, common.expectsError('TEST_ERROR_1', RangeError)); | ||
}, /^AssertionError: error is not the expected type/); | ||
|
||
assert.throws(() => { | ||
assert.throws(() => { | ||
throw new errors.TypeError('TEST_ERROR_1', 'a'); | ||
}, common.expectsError('TEST_ERROR_1', TypeError, /^Error for testing 2/)); | ||
}, /^AssertionError: error.message does not match/); |