Skip to content

Commit 5f8d0bf

Browse files
committed
feat: create custom Error constructors
As a result of nodejs/node#33857, Node errors are no longer custom error classes and .constructor is a built-in Error constructor. Therefore, to support Node 15 and later, define Error constructors for the codes used by this module in this module, as done by readable-stream and other projects. Note: Much of the code is copied from lib/internal/errors.js @ v15.0.1. ESLint rules are relaxed to make it easier to minimize divergence and make updates easier. Signed-off-by: Kevin Locke <kevin@kevinlocke.name>
1 parent 27a64d5 commit 5f8d0bf

2 files changed

Lines changed: 262 additions & 58 deletions

File tree

.eslintrc.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,23 @@
2828
"node/shebang": "off"
2929
}
3030
},
31+
{
32+
"files": [
33+
"lib/errors.js"
34+
],
35+
"rules": {
36+
"comma-dangle": ["error", "only-multiline"],
37+
"curly": "off",
38+
"global-require": "off",
39+
"indent": "off",
40+
"new-cap": "off",
41+
"no-restricted-syntax": "off",
42+
"no-use-before-define": ["error", { "functions": false }],
43+
"nonblock-statement-body-position": ["error", "below"],
44+
"operator-linebreak": ["error", "after"],
45+
"unicorn/no-null": "off"
46+
}
47+
},
3148
{
3249
"files": [
3350
"lib/zlib-internal.js"

lib/errors.js

Lines changed: 245 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,260 @@
11
/**
2-
* Export error constructors (or work-alikes) from lib/internal/errors.js
2+
* Caller-visible Errors thrown by this module.
33
*
4-
* These are ugly hacks. Hopefully the constructors will be exposed in a
5-
* future version: https://github.com/nodejs/node/issues/14554
4+
* Based on Node.js core errors in lib/internal/errors.js @ v15.0.1.
65
*
6+
* Hopefully the constructors will be exposed in a future version:
7+
* https://github.com/nodejs/node/issues/14554
8+
*
9+
* Copies are already proliferating:
10+
* https://github.com/nodejs/readable-stream/blob/v3.6.0/errors.js
11+
* https://github.com/streamich/memfs/blob/v3.2.0/src/internal/errors.ts
12+
*
13+
* Looks like there was an attempt to create a standalone module:
14+
* https://github.com/jasnell/internal-errors
15+
*
16+
* @copyright Copyright Joyent, Inc. and other Node contributors.
717
* @copyright Copyright 2020 Kevin Locke <kevin@kevinlocke.name>
818
* @license MIT
919
*/
1020

1121
'use strict';
1222

23+
const ArrayIsArray = Array.isArray;
24+
const ObjectDefineProperty = Object.defineProperty;
25+
26+
const messages = new Map();
27+
const codes = exports;
28+
29+
const classRegExp = /^([A-Z][a-z0-9]*)+$/;
30+
// Sorted by a rough estimate on most frequently used entries.
31+
const kTypes = [
32+
'string',
33+
'function',
34+
'number',
35+
'object',
36+
// Accept 'Function' and 'Object' as alternative to the lower cased version.
37+
'Function',
38+
'Object',
39+
'boolean',
40+
'bigint',
41+
'symbol'
42+
];
43+
44+
let excludedStackFn;
45+
46+
let internalUtilInspect = null;
47+
function lazyInternalUtilInspect() {
48+
if (!internalUtilInspect) {
49+
internalUtilInspect = require('util');
50+
}
51+
return internalUtilInspect;
52+
}
53+
1354
const assert = require('assert');
14-
const { kMaxLength } = require('buffer');
15-
const { Readable, finished } = require('stream');
16-
const { Deflate, deflate } = require('zlib');
17-
18-
// Get ERR_BUFFER_TOO_LARGE by monkey-patching .end() to pretend more than
19-
// kMaxLength bytes have been read.
20-
assert(
21-
!hasOwnProperty.call(Deflate.prototype, 'end'),
22-
'Deflate.prototype does not define end',
23-
);
24-
Deflate.prototype.end = function() {
25-
this.nread = kMaxLength + 1;
26-
// Hit check in zlibBufferOnData for node >= 14.6.0 (ec804f231f)
27-
this.emit('data', Buffer.alloc(0));
28-
// Hit check in zlibBufferOnEnd for node < 14.6.0 (ec804f231f)
29-
this.close = () => {};
30-
this.emit('end');
31-
};
32-
try {
33-
deflate(Buffer.alloc(0), (err) => {
34-
assert.strictEqual(err && err.code, 'ERR_BUFFER_TOO_LARGE');
35-
exports.ERR_BUFFER_TOO_LARGE = err.constructor;
36-
});
37-
} finally {
38-
delete Deflate.prototype.end;
55+
56+
function makeNodeErrorWithCode(Base, key) {
57+
return function NodeError(...args) {
58+
let error;
59+
if (excludedStackFn === undefined) {
60+
error = new Base();
61+
} else {
62+
const limit = Error.stackTraceLimit;
63+
Error.stackTraceLimit = 0;
64+
error = new Base();
65+
// Reset the limit and setting the name property.
66+
Error.stackTraceLimit = limit;
67+
}
68+
const message = getMessage(key, args, error);
69+
ObjectDefineProperty(error, 'message', {
70+
value: message,
71+
enumerable: false,
72+
writable: true,
73+
configurable: true,
74+
});
75+
ObjectDefineProperty(error, 'toString', {
76+
value() {
77+
return `${this.name} [${key}]: ${this.message}`;
78+
},
79+
enumerable: false,
80+
writable: true,
81+
configurable: true,
82+
});
83+
addCodeToName(error, Base.name, key);
84+
error.code = key;
85+
return error;
86+
};
87+
}
88+
89+
function addCodeToName(err, name, code) {
90+
// Set the stack
91+
if (excludedStackFn !== undefined) {
92+
Error.captureStackTrace(err, excludedStackFn);
93+
}
94+
// Add the error code to the name to include it in the stack trace.
95+
err.name = `${name} [${code}]`;
96+
// Access the stack to generate the error message including the error code
97+
// from the name.
98+
// eslint-disable-next-line no-unused-expressions
99+
err.stack;
100+
// Reset the name to the actual name.
101+
if (name === 'SystemError') {
102+
ObjectDefineProperty(err, 'name', {
103+
value: name,
104+
enumerable: false,
105+
writable: true,
106+
configurable: true
107+
});
108+
} else {
109+
delete err.name;
110+
}
111+
}
112+
113+
// Utility function for registering the error codes. Only used here. Exported
114+
// *only* to allow for testing.
115+
function E(sym, val, def) {
116+
messages.set(sym, val);
117+
def = makeNodeErrorWithCode(def, sym);
118+
codes[sym] = def;
39119
}
40-
assert(
41-
exports.ERR_BUFFER_TOO_LARGE,
42-
'zlib.deflate calls callback immediately on error',
43-
);
44-
45-
// Get ERR_INVALID_ARG_TYPE by calling Buffer.alloc with an invalid type
46-
try {
47-
Buffer.alloc(true);
48-
} catch (err) {
49-
assert.strictEqual(err.code, 'ERR_INVALID_ARG_TYPE');
50-
exports.ERR_INVALID_ARG_TYPE = err.constructor;
120+
121+
function getMessage(key, args, self) {
122+
const msg = messages.get(key);
123+
124+
if (typeof msg === 'function') {
125+
assert(
126+
msg.length <= args.length, // Default options do not count.
127+
`Code: ${key}; The provided arguments length (${args.length}) does not ` +
128+
`match the required ones (${msg.length}).`
129+
);
130+
return msg.apply(self, args);
131+
}
132+
133+
const expectedLength = (msg.match(/%[dfijoOs]/g) || []).length;
134+
assert(
135+
expectedLength === args.length,
136+
`Code: ${key}; The provided arguments length (${args.length}) does not ` +
137+
`match the required ones (${expectedLength}).`
138+
);
139+
if (args.length === 0)
140+
return msg;
141+
142+
args.unshift(msg);
143+
return lazyInternalUtilInspect().format.apply(null, args);
51144
}
52-
assert(
53-
exports.ERR_INVALID_ARG_TYPE,
54-
'Buffer.alloc throws for Boolean argument',
55-
);
56-
57-
// Get ERR_STREAM_PREMATURE_CLOSE using stream.finish
58-
const readable = new Readable();
59-
finished(readable, (err) => {
60-
assert.strictEqual(err && err.code, 'ERR_STREAM_PREMATURE_CLOSE');
61-
exports.ERR_STREAM_PREMATURE_CLOSE = err.constructor;
62-
});
63-
readable.emit('close');
64-
assert(
65-
exports.ERR_STREAM_PREMATURE_CLOSE,
66-
'stream.finished calls callback on close',
67-
);
68-
69-
// eslint-disable-next-line unicorn/custom-error-definition
70-
exports.ERR_SYNC_NOT_SUPPORTED = class InflateAutoError extends Error {
145+
146+
E('ERR_BUFFER_TOO_LARGE',
147+
'Cannot create a Buffer larger than %s bytes',
148+
RangeError);
149+
E('ERR_INVALID_ARG_TYPE',
150+
(name, expected, actual) => {
151+
assert(typeof name === 'string', "'name' must be a string");
152+
if (!ArrayIsArray(expected)) {
153+
expected = [expected];
154+
}
155+
156+
let msg = 'The ';
157+
if (name.endsWith(' argument')) {
158+
// For cases like 'first argument'
159+
msg += `${name} `;
160+
} else {
161+
const type = name.includes('.') ? 'property' : 'argument';
162+
msg += `"${name}" ${type} `;
163+
}
164+
msg += 'must be ';
165+
166+
const types = [];
167+
const instances = [];
168+
const other = [];
169+
170+
for (const value of expected) {
171+
assert(typeof value === 'string',
172+
'All expected entries have to be of type string');
173+
if (kTypes.includes(value)) {
174+
types.push(value.toLowerCase());
175+
} else if (classRegExp.test(value)) {
176+
instances.push(value);
177+
} else {
178+
assert(value !== 'object',
179+
'The value "object" should be written as "Object"');
180+
other.push(value);
181+
}
182+
}
183+
184+
// Special handle `object` in case other instances are allowed to outline
185+
// the differences between each other.
186+
if (instances.length > 0) {
187+
const pos = types.indexOf('object');
188+
if (pos !== -1) {
189+
types.splice(pos, 1);
190+
instances.push('Object');
191+
}
192+
}
193+
194+
if (types.length > 0) {
195+
if (types.length > 2) {
196+
const last = types.pop();
197+
msg += `one of type ${types.join(', ')}, or ${last}`;
198+
} else if (types.length === 2) {
199+
msg += `one of type ${types[0]} or ${types[1]}`;
200+
} else {
201+
msg += `of type ${types[0]}`;
202+
}
203+
if (instances.length > 0 || other.length > 0)
204+
msg += ' or ';
205+
}
206+
207+
if (instances.length > 0) {
208+
if (instances.length > 2) {
209+
const last = instances.pop();
210+
msg += `an instance of ${instances.join(', ')}, or ${last}`;
211+
} else {
212+
msg += `an instance of ${instances[0]}`;
213+
if (instances.length === 2) {
214+
msg += ` or ${instances[1]}`;
215+
}
216+
}
217+
if (other.length > 0)
218+
msg += ' or ';
219+
}
220+
221+
if (other.length > 0) {
222+
if (other.length > 2) {
223+
const last = other.pop();
224+
msg += `one of ${other.join(', ')}, or ${last}`;
225+
} else if (other.length === 2) {
226+
msg += `one of ${other[0]} or ${other[1]}`;
227+
} else {
228+
if (other[0].toLowerCase() !== other[0])
229+
msg += 'an ';
230+
msg += `${other[0]}`;
231+
}
232+
}
233+
234+
if (actual == null) {
235+
msg += `. Received ${actual}`;
236+
} else if (typeof actual === 'function' && actual.name) {
237+
msg += `. Received function ${actual.name}`;
238+
} else if (typeof actual === 'object') {
239+
if (actual.constructor && actual.constructor.name) {
240+
msg += `. Received an instance of ${actual.constructor.name}`;
241+
} else {
242+
const inspected = lazyInternalUtilInspect()
243+
.inspect(actual, { depth: -1 });
244+
msg += `. Received ${inspected}`;
245+
}
246+
} else {
247+
let inspected = lazyInternalUtilInspect()
248+
.inspect(actual, { colors: false });
249+
if (inspected.length > 25)
250+
inspected = `${inspected.slice(0, 25)}...`;
251+
msg += `. Received type ${typeof actual} (${inspected})`;
252+
}
253+
return msg;
254+
}, TypeError);
255+
E('ERR_STREAM_PREMATURE_CLOSE', 'Premature close', Error);
256+
257+
codes.ERR_SYNC_NOT_SUPPORTED = class InflateAutoError extends Error {
71258
constructor(target) {
72259
super();
73260
let message = 'Synchronous operation is not supported';

0 commit comments

Comments
 (0)