-
Notifications
You must be signed in to change notification settings - Fork 30.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
errors: add internal/errors.js #11220
Conversation
doc/guides/using-internal-errors.md
Outdated
|
||
## Adding new errors | ||
|
||
New static error codes are adding by modifying the `internal/errors.js` file |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/adding/added/
E('EXAMPLE_KEY2', (a, b) => return `${a} ${b}`); | ||
``` | ||
|
||
The first argument passed to `E()` is the static identifier. The second |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I understand correctly, the name of this function being so short is to echo the existing convention for error names like ENOTFOUND
? But this convention is no longer followed in this new error code system?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is short to make it as brief as possible. It is intended to be used only within this file. It is exported only to facilitate testing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, but this is a guide so I think this would be read by contributors, especially those who are new? And they might need to change that file and call E
to register new errors.
doc/guides/using-internal-errors.md
Outdated
### Class: errors.Error(key[, args...]) | ||
|
||
* `key` {String} The static error identifier | ||
* `args... {Any} Zero or more optional arguments |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are the arguments left undocumented because they are non-standard? Probably an explanation would help readers to understand what they are(e.g. a link to MDN)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The arguments are arbitrary and dependent on the specific error message (essentially the same as util.format()
. Specifically, the error message for a key may be something like 'Foo %s %d'
, so that errors.Error('KEY', 's', '1')
would produce the error message 'Foo s 1'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the explanation, but does this built-in errors.Error()
method have a formatting function defined? It seems to just pass the arguments to the Error
base class, so we already know what the format would be(at least for V8)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or, do we allow users to customize the format of these builtin errors?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Look at the message()
function. When a key is registered using the E()
function, it's associated value is either a string with optional util.format tokens or a function that returns a string. Inside message()
, the key lookup returns this value. If the value is a string, it is passed through util.format()
, otherwise the arguments are passed to the function, allowing more customized formatting for the error.
Again, this is not a user facing API. This is for use only within core and would be used anywhere we throw an internal error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, yes, but "users" I meant "consumers of this API, i.e. contributors". I think my questions have been answered, thanks.
On a side note, I think I was having questions because the formatting behavior is explained near the bottom of this document, so by the time I read this, I was not sure how the arg
s are going to be consumed. A reference to the formatting process might help, or maybe the formatting process can be explained first, but maybe that's just me :).
lib/internal/errors.js
Outdated
class NodeError extends Error { | ||
constructor(key /**, ...args **/) { | ||
lazyAssert().strictEqual(typeof key, 'string'); | ||
const args = Array(arguments.length - 1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we can just use the rest operator? Probably faster in newer versions of V8.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rest parameters are not yet fully optimized. There is a benchmark already to test those. Once they have been optimized we can switch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you refer to benchmark/misc/console.js
? I just run it with the latest master and it seems the usage with arguments is already optimized?
./node benchmark/run.js --filter console misc
misc/console.js
misc/console.js n=1000000 concat=1 method="restAndSpread": 499,544.4494311414
misc/console.js n=1000000 concat=0 method="restAndSpread": 513,992.8268692781
misc/console.js n=1000000 concat=1 method="argumentsAndApply": 619,756.4629049919
misc/console.js n=1000000 concat=0 method="argumentsAndApply": 624,507.957596242
misc/console.js n=1000000 concat=1 method="restAndApply": 688,473.7438723565
misc/console.js n=1000000 concat=0 method="restAndApply": 681,323.2961384911
misc/console.js n=1000000 concat=1 method="restAndConcat": 895,355.2343246405
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's possible that things have improved with the most recent v8 drops. I'm running the benchmark/es/restparams-bench.js
right now to compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, yes, those are the right benchmarks.
Also I discovered something interesting. If I leave the forced optimization there, the results are being consistent with:
es/restparams-bench.js
es/restparams-bench.js millions=100 method="copy": 30.148206272683915
es/restparams-bench.js millions=100 method="rest": 22.696521524914942
es/restparams-bench.js millions=100 method="arguments": 55.12597317932212
But if I comment out the forced optimization:
es/restparams-bench.js
es/restparams-bench.js millions=100 method="copy": 30.754392030574046
es/restparams-bench.js millions=100 method="rest": 33.315344080305636
es/restparams-bench.js millions=100 method="arguments": 57.77862383401849
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, very nice, these results are MUCH better than they were just a couple of months ago. Switching to rest params!!
improvement confidence p.value
es/restparams-bench.js millions=100 method="arguments" -0.17 % 0.9399783
es/restparams-bench.js millions=100 method="copy" 0.02 % 0.9923623
es/restparams-bench.js millions=100 method="rest" 4.06 % 0.1729710
lib/internal/errors.js
Outdated
|
||
class NodeError extends Error { | ||
constructor(key /**, ...args **/) { | ||
lazyAssert().strictEqual(typeof key, 'string'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A more informative message would be helpful here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This assert is not intended to be user facing. This is simply to make sure proper use of the internal API and is here only to make sure that node itself is passing in the right kind of value.
lib/internal/errors.js
Outdated
function message(key, args) { | ||
const assert = lazyAssert(); | ||
const util = lazyUtil(); | ||
const msg = exports[symbolFor(key)]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I am reading this right, the second time a key is passed into this, it will be in the map and this assertion won't fail, even though the error is not properly registered with E
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah, right, good point.
assert.strictEqual(err4.message, 'abc xyz'); | ||
assert.strictEqual(err4.code, 'TEST_ERROR_2'); | ||
|
||
assert.throws( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if this block is executed twice?(TEST_FOO_KEY
gets passed twice)
doc/guides/using-internal-errors.md
Outdated
The `internal/errors` module exposes three custom `Error` classes as a | ||
`message` function. | ||
|
||
### Class: errors.Error(key[, args...]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we now have rest parameters, I’d be in favour of using ...args
in these places if that’s possible?
doc/guides/using-internal-errors.md
Outdated
### Class: errors.RangeError(key[, args...]) | ||
|
||
* `key` {String} The static error identifier | ||
* `args... {Any} Zero or more optional arguments |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There’s a missing backtick on all copies of this line. :D
lib/internal/errors.js
Outdated
return util; | ||
} | ||
|
||
class NodeError extends Error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this can be a mixin to avoid code duplication?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Possibly. I did it this way in order to preserve instanceof
checks (e.g. (new error.TypeError()) instanceof TypeError
) but may be able to find a way around that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure, would that really be much of a problem? What I had in mind was something like this:
const makeNodeError = (Base) => class NodeError extends Base {
[… current code, except only once …]
static get name() { return `Node${super.name}`; } /* optional */
};
const NodeError = makeNodeError(Error);
const NodeRangeError = makeNodeError(RangeError);
const NodeTypeError = makeNodeError(TypeError);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah right :-) .. that works too
871565c
to
aa81f06
Compare
Updated! PTAL |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good, thanks for (re-)starting this!
And by the way, I don’t think this really counts semver-minor if it doesn’t touch the public API. (Probably makes no difference, though. 😄)
doc/guides/using-internal-errors.md
Outdated
file: | ||
|
||
```js | ||
E('FOO', 'Expected string received %s); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing quote ;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
grr.. lol ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm, the linter doesn't lint code inside doc/guides?
return msg; | ||
args.unshift(msg); | ||
} | ||
return String(fmt.apply(null, args)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fmt(...args);
? Not sure whether that was part of your conversation with @joyeecheung or not
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That conversation is about rest operator and this is spread operator...not sure about the performance of spread though? From the results of benchmark/misc/console.js
with method="restAndSpread"
compared method="restAndApply"
I think the spread operator is not ready yet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't look like we have a benchmark for the spread operator so I'll open a PR to add one
lib/internal/errors.js
Outdated
kSymbolsMap.set(name, sym); | ||
} | ||
return sym; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure I understand why this file uses symbol properties of exports
… wouldn’t const messages = new Map();
as a key → message map be a bit easier?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah doesn't really make much sense now. This was originally done to make it easier to discover/enumerate these but that originated in a much older version
aa81f06
to
6f2fe59
Compare
Failure in CI is unrelated. |
So, one concern I have is how will we backport these? Will we set the error codes on master and then just backport? If so, what happens if a new error is added to just a release line, but not to master? |
This would likely need to land in Node.js 8.0.0 and not be backported. The PRs to port existing errors to this mechanism would be semver-major initially so would no backport to older majors. Only changes after porting would be semver-minor or patch. As much as possible we should try to avoid adding new types of errors (new error codes). On the off chance that we are forced to land something in a release line and not master, those would land in that release line and should be added to the documentation in master but not necessarily be used in master. Hopefully that made sense. |
Thanks @jasnell! That makes sense to me. Just wanted to make sure it had been thought through. :] |
doc/guides/using-internal-errors.md
Outdated
static, permanent error code and an optionally parameterized message. The | ||
intent of the module is to make errors thrown by Node.js core to be more | ||
consistent and to allow changes to error messages to be made without requiring | ||
a semver-major label. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: semver-major label
is kind of our internal jargon and people aren't going to understand (without an explanation) why we consider error message changes as semver major anyway.
Maybe just embrace completeness as this is a guide and go with something more like this?:
...parameterized message. [END OF PARAGRAPH]
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.
@jasnell This feature is long overdue. Thanks for doing all the work to get us to the point where we can do this! |
Add the internal/errors.js core mechanism.
6f2fe59
to
08f6372
Compare
@Trott, updated with your suggestion in the guide |
New CI before landing: https://ci.nodejs.org/job/node-test-pull-request/6315/ |
Add the internal/errors.js core mechanism. PR-URL: #11220 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
Landed in 159749d |
Add the internal/errors.js core mechanism. PR-URL: #11220 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
@@ -616,3 +616,18 @@ exports.WPT = { | |||
assert.fail(undefined, undefined, `Reached unreachable code: ${desc}`); | |||
} | |||
}; | |||
|
|||
// Useful for testing expected internal/error objects | |||
exports.expectsError = function expectsError(code, type, message) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function was added to common.js
but not documented in the test/README.md
.
Why? None of those existing errors or codes are affected by this in any way. The |
To be certain, this mechanism does not change or override any of the existing core uses of |
Hmm, so for now we just leave the |
For now, yes. Eventually we should work on making all of the errors consistent, but for those errors that already have a |
} | ||
|
||
get code() { | ||
return this[kCode]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, so if a PR updates a module to throw the new errors, since code
is read only here, any code setting .code
later would not have effect, then this could break existing behavior unless we check carefully enough before landing those PRs? Can we add a setter that do some kinds of assertion here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For example:
set code(options) {
if (typeof options === 'object' && options.override === true) {
this[kCode] = options.code;
return;
}
assert.fail('This error can not be migrated for now');
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
EDIT: forgot return
^
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd rather just leave it read only. If someone really wanted to override the value of .code
, they can easily use Object.defineProperty()
to do so.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
then just raise the assertion then? This is just to prevent we accidently migrate an error that already have its .code
set by existing code (but it could be hard to catch if that code happens much later than its creation, like in a callback), and by throwing this new kind of error, that code can't actually set .code
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are no cases that I'm aware of where we set the code later. And in the case a user attempts to set the code, I'd rather they not see an assertion error. We can best catch these cases in code review
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, that can not be let loose in the user land. Anyway thanks for bearing with me :P
I think @Trott brings up a good point... I'm not sure how I feel about error messages changing from |
I'm still not sure where I stand on this. I'm trying to work out the cost/benefit on this. Like, if I thought that changing the error message to that would mostly-eliminate full-message-text-sniffing and therefore allow us to treat message changes as semver-patch, I'd probably be all for it. But I'm not sure I can convince myself that will happen. And there's a potential of a lot of adverse effects on the ecosystem from this. |
Serious question: Like what? The code is included in the The The actual For each of these errors, |
This is a total nit, but the lack of space before the opening square bracket suggests it's an index or property operator and not for human consumption. I wonder if it would be better to use parentheses and to use a space. Maybe put it on the other side of the colon while we're at it:
|
Putting it on the other side of the colon implies that we're either customizing the |
Anything that uses You raise some good points I didn't realize that mitigate things. I guess CITGM runs on the various PRs can help provide some data about how widespread issues might be. |
Yep, uses of |
I think these two things are very different:
|
Yep, which is why changes need to happen incrementally. The initial version of this pr included changes for everything, all at once. I backed off that so that individual changes can be looked at case by case |
BTW, I'm not arguing against this. I'm still trying to assess it. The fact that this will change pretty much all (or at least a very large number of) core error messages was a surprise to me. Judging from his comments, it surprised @evanlucas as well. Small data set, yes. It's entirely possible this is totally the way to go. |
Add the internal/errors.js core mechanism. PR-URL: nodejs#11220 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
Add the internal/errors.js core mechanism. PR-URL: nodejs#11220 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
Add the internal/errors.js core mechanism.
This was extracted from #9265 with some updates. It adds the basic mechanism for moving to the internal errors module without modifying any of the actual error instances within core. Transitioning to the use of this module should be done incrementally in small chunks over time.
Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passesAffected core subsystem(s)
errors