Skip to content

Commit

Permalink
feat: create custom Error constructors
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
kevinoid committed Oct 27, 2020
1 parent 27a64d5 commit 5f8d0bf
Show file tree
Hide file tree
Showing 2 changed files with 262 additions and 58 deletions.
17 changes: 17 additions & 0 deletions .eslintrc.json
Expand Up @@ -28,6 +28,23 @@
"node/shebang": "off"
}
},
{
"files": [
"lib/errors.js"
],
"rules": {
"comma-dangle": ["error", "only-multiline"],
"curly": "off",
"global-require": "off",
"indent": "off",
"new-cap": "off",
"no-restricted-syntax": "off",
"no-use-before-define": ["error", { "functions": false }],
"nonblock-statement-body-position": ["error", "below"],
"operator-linebreak": ["error", "after"],
"unicorn/no-null": "off"
}
},
{
"files": [
"lib/zlib-internal.js"
Expand Down
303 changes: 245 additions & 58 deletions lib/errors.js
@@ -1,73 +1,260 @@
/**
* Export error constructors (or work-alikes) from lib/internal/errors.js
* Caller-visible Errors thrown by this module.
*
* These are ugly hacks. Hopefully the constructors will be exposed in a
* future version: https://github.com/nodejs/node/issues/14554
* Based on Node.js core errors in lib/internal/errors.js @ v15.0.1.
*
* Hopefully the constructors will be exposed in a future version:
* https://github.com/nodejs/node/issues/14554
*
* Copies are already proliferating:
* https://github.com/nodejs/readable-stream/blob/v3.6.0/errors.js
* https://github.com/streamich/memfs/blob/v3.2.0/src/internal/errors.ts
*
* Looks like there was an attempt to create a standalone module:
* https://github.com/jasnell/internal-errors
*
* @copyright Copyright Joyent, Inc. and other Node contributors.
* @copyright Copyright 2020 Kevin Locke <kevin@kevinlocke.name>
* @license MIT
*/

'use strict';

const ArrayIsArray = Array.isArray;
const ObjectDefineProperty = Object.defineProperty;

const messages = new Map();
const codes = exports;

const classRegExp = /^([A-Z][a-z0-9]*)+$/;
// Sorted by a rough estimate on most frequently used entries.
const kTypes = [
'string',
'function',
'number',
'object',
// Accept 'Function' and 'Object' as alternative to the lower cased version.
'Function',
'Object',
'boolean',
'bigint',
'symbol'
];

let excludedStackFn;

let internalUtilInspect = null;
function lazyInternalUtilInspect() {
if (!internalUtilInspect) {
internalUtilInspect = require('util');
}
return internalUtilInspect;
}

const assert = require('assert');
const { kMaxLength } = require('buffer');
const { Readable, finished } = require('stream');
const { Deflate, deflate } = require('zlib');

// Get ERR_BUFFER_TOO_LARGE by monkey-patching .end() to pretend more than
// kMaxLength bytes have been read.
assert(
!hasOwnProperty.call(Deflate.prototype, 'end'),
'Deflate.prototype does not define end',
);
Deflate.prototype.end = function() {
this.nread = kMaxLength + 1;
// Hit check in zlibBufferOnData for node >= 14.6.0 (ec804f231f)
this.emit('data', Buffer.alloc(0));
// Hit check in zlibBufferOnEnd for node < 14.6.0 (ec804f231f)
this.close = () => {};
this.emit('end');
};
try {
deflate(Buffer.alloc(0), (err) => {
assert.strictEqual(err && err.code, 'ERR_BUFFER_TOO_LARGE');
exports.ERR_BUFFER_TOO_LARGE = err.constructor;
});
} finally {
delete Deflate.prototype.end;

function makeNodeErrorWithCode(Base, key) {
return function NodeError(...args) {
let error;
if (excludedStackFn === undefined) {
error = new Base();
} else {
const limit = Error.stackTraceLimit;
Error.stackTraceLimit = 0;
error = new Base();
// Reset the limit and setting the name property.
Error.stackTraceLimit = limit;
}
const message = getMessage(key, args, error);
ObjectDefineProperty(error, 'message', {
value: message,
enumerable: false,
writable: true,
configurable: true,
});
ObjectDefineProperty(error, 'toString', {
value() {
return `${this.name} [${key}]: ${this.message}`;
},
enumerable: false,
writable: true,
configurable: true,
});
addCodeToName(error, Base.name, key);
error.code = key;
return error;
};
}

function addCodeToName(err, name, code) {
// Set the stack
if (excludedStackFn !== undefined) {
Error.captureStackTrace(err, excludedStackFn);
}
// Add the error code to the name to include it in the stack trace.
err.name = `${name} [${code}]`;
// Access the stack to generate the error message including the error code
// from the name.
// eslint-disable-next-line no-unused-expressions
err.stack;
// Reset the name to the actual name.
if (name === 'SystemError') {
ObjectDefineProperty(err, 'name', {
value: name,
enumerable: false,
writable: true,
configurable: true
});
} else {
delete err.name;
}
}

// Utility function for registering the error codes. Only used here. Exported
// *only* to allow for testing.
function E(sym, val, def) {
messages.set(sym, val);
def = makeNodeErrorWithCode(def, sym);
codes[sym] = def;
}
assert(
exports.ERR_BUFFER_TOO_LARGE,
'zlib.deflate calls callback immediately on error',
);

// Get ERR_INVALID_ARG_TYPE by calling Buffer.alloc with an invalid type
try {
Buffer.alloc(true);
} catch (err) {
assert.strictEqual(err.code, 'ERR_INVALID_ARG_TYPE');
exports.ERR_INVALID_ARG_TYPE = err.constructor;

function getMessage(key, args, self) {
const msg = messages.get(key);

if (typeof msg === 'function') {
assert(
msg.length <= args.length, // Default options do not count.
`Code: ${key}; The provided arguments length (${args.length}) does not ` +
`match the required ones (${msg.length}).`
);
return msg.apply(self, args);
}

const expectedLength = (msg.match(/%[dfijoOs]/g) || []).length;
assert(
expectedLength === args.length,
`Code: ${key}; The provided arguments length (${args.length}) does not ` +
`match the required ones (${expectedLength}).`
);
if (args.length === 0)
return msg;

args.unshift(msg);
return lazyInternalUtilInspect().format.apply(null, args);
}
assert(
exports.ERR_INVALID_ARG_TYPE,
'Buffer.alloc throws for Boolean argument',
);

// Get ERR_STREAM_PREMATURE_CLOSE using stream.finish
const readable = new Readable();
finished(readable, (err) => {
assert.strictEqual(err && err.code, 'ERR_STREAM_PREMATURE_CLOSE');
exports.ERR_STREAM_PREMATURE_CLOSE = err.constructor;
});
readable.emit('close');
assert(
exports.ERR_STREAM_PREMATURE_CLOSE,
'stream.finished calls callback on close',
);

// eslint-disable-next-line unicorn/custom-error-definition
exports.ERR_SYNC_NOT_SUPPORTED = class InflateAutoError extends Error {

E('ERR_BUFFER_TOO_LARGE',
'Cannot create a Buffer larger than %s bytes',
RangeError);
E('ERR_INVALID_ARG_TYPE',
(name, expected, actual) => {
assert(typeof name === 'string', "'name' must be a string");
if (!ArrayIsArray(expected)) {
expected = [expected];
}

let msg = 'The ';
if (name.endsWith(' argument')) {
// For cases like 'first argument'
msg += `${name} `;
} else {
const type = name.includes('.') ? 'property' : 'argument';
msg += `"${name}" ${type} `;
}
msg += 'must be ';

const types = [];
const instances = [];
const other = [];

for (const value of expected) {
assert(typeof value === 'string',
'All expected entries have to be of type string');
if (kTypes.includes(value)) {
types.push(value.toLowerCase());
} else if (classRegExp.test(value)) {
instances.push(value);
} else {
assert(value !== 'object',
'The value "object" should be written as "Object"');
other.push(value);
}
}

// Special handle `object` in case other instances are allowed to outline
// the differences between each other.
if (instances.length > 0) {
const pos = types.indexOf('object');
if (pos !== -1) {
types.splice(pos, 1);
instances.push('Object');
}
}

if (types.length > 0) {
if (types.length > 2) {
const last = types.pop();
msg += `one of type ${types.join(', ')}, or ${last}`;
} else if (types.length === 2) {
msg += `one of type ${types[0]} or ${types[1]}`;
} else {
msg += `of type ${types[0]}`;
}
if (instances.length > 0 || other.length > 0)
msg += ' or ';
}

if (instances.length > 0) {
if (instances.length > 2) {
const last = instances.pop();
msg += `an instance of ${instances.join(', ')}, or ${last}`;
} else {
msg += `an instance of ${instances[0]}`;
if (instances.length === 2) {
msg += ` or ${instances[1]}`;
}
}
if (other.length > 0)
msg += ' or ';
}

if (other.length > 0) {
if (other.length > 2) {
const last = other.pop();
msg += `one of ${other.join(', ')}, or ${last}`;
} else if (other.length === 2) {
msg += `one of ${other[0]} or ${other[1]}`;
} else {
if (other[0].toLowerCase() !== other[0])
msg += 'an ';
msg += `${other[0]}`;
}
}

if (actual == null) {
msg += `. Received ${actual}`;
} else if (typeof actual === 'function' && actual.name) {
msg += `. Received function ${actual.name}`;
} else if (typeof actual === 'object') {
if (actual.constructor && actual.constructor.name) {
msg += `. Received an instance of ${actual.constructor.name}`;
} else {
const inspected = lazyInternalUtilInspect()
.inspect(actual, { depth: -1 });
msg += `. Received ${inspected}`;
}
} else {
let inspected = lazyInternalUtilInspect()
.inspect(actual, { colors: false });
if (inspected.length > 25)
inspected = `${inspected.slice(0, 25)}...`;
msg += `. Received type ${typeof actual} (${inspected})`;
}
return msg;
}, TypeError);
E('ERR_STREAM_PREMATURE_CLOSE', 'Premature close', Error);

codes.ERR_SYNC_NOT_SUPPORTED = class InflateAutoError extends Error {
constructor(target) {
super();
let message = 'Synchronous operation is not supported';
Expand Down

0 comments on commit 5f8d0bf

Please sign in to comment.