diff --git a/packages/common/NEWS.md b/packages/common/NEWS.md
index e69de29bb2..59e2df12c3 100644
--- a/packages/common/NEWS.md
+++ b/packages/common/NEWS.md
@@ -0,0 +1,18 @@
+User-visible changes in `@endo/common`:
+
+# next release
+
+- Change to `throwLabeled`
+ - Like the assertion functions/methods that were parameterized by an error
+ constructor (`makeError`, `assert`, `assert.fail`, `assert.equal`),
+ `throwLabeled` now also accepts named options `cause` and `errors` in its
+ immediately succeeding `options` argument.
+ - Like those assertion functions, the error constructor argument to
+ `throwLabeled` can now be an `AggregateError`.
+ If `throwLabeled` makes an error instance, it encapsulates the
+ non-uniformity of the `AggregateError` construction arguments, allowing
+ all the error constructors to be used polymorphically
+ (generic / interchangeable).
+ - The error constructor argument is now typed `GenericErrorConstructor`,
+ effectively the common supertype of `ErrorConstructor` and
+ `AggregateErrorConstructor`.
diff --git a/packages/common/throw-labeled.js b/packages/common/throw-labeled.js
index b7457ecf4f..1c3709de63 100644
--- a/packages/common/throw-labeled.js
+++ b/packages/common/throw-labeled.js
@@ -7,14 +7,24 @@ import { X, makeError, annotateError } from '@endo/errors';
*
* @param {Error} innerErr
* @param {string|number} label
- * @param {ErrorConstructor=} ErrorConstructor
+ * @param {import('ses').GenericErrorConstructor} [errConstructor]
+ * @param {import('ses').AssertMakeErrorOptions} [options]
* @returns {never}
*/
-export const throwLabeled = (innerErr, label, ErrorConstructor = undefined) => {
+export const throwLabeled = (
+ innerErr,
+ label,
+ errConstructor = undefined,
+ options = undefined,
+) => {
if (typeof label === 'number') {
label = `[${label}]`;
}
- const outerErr = makeError(`${label}: ${innerErr.message}`, ErrorConstructor);
+ const outerErr = makeError(
+ `${label}: ${innerErr.message}`,
+ errConstructor,
+ options,
+ );
annotateError(outerErr, X`Caused by ${innerErr}`);
throw outerErr;
};
diff --git a/packages/common/tsconfig.json b/packages/common/tsconfig.json
index f77b8008a1..20335e4343 100644
--- a/packages/common/tsconfig.json
+++ b/packages/common/tsconfig.json
@@ -1,5 +1,9 @@
{
"extends": "../../tsconfig.eslint-base.json",
+ "compilerOptions": {
+ "checkJs": true,
+ "maxNodeModuleJsDepth": 1,
+ },
"include": [
"*.js",
"*.ts",
diff --git a/packages/errors/NEWS.md b/packages/errors/NEWS.md
new file mode 100644
index 0000000000..721867a95e
--- /dev/null
+++ b/packages/errors/NEWS.md
@@ -0,0 +1,20 @@
+User-visible changes in `@endo/errors`:
+
+# next release
+
+- `AggegateError` support
+ - Assertion functions/methods that were parameterized by an error constructor
+ (`makeError`, `assert`, `assert.fail`, `assert.equal`) now also accept named
+ options `cause` and `errors` in their immediately succeeding
+ `options` argument.
+ - For all those, the error constructor can now be an `AggregateError`.
+ If they do make an error instance, they encapsulate the
+ non-uniformity of the `AggregateError` construction arguments, allowing
+ all the error constructors to be used polymorphically
+ (generic / interchangeable).
+ - Adds a `GenericErrorConstructor` type to be effectively the common supertype
+ of `ErrorConstructor` and `AggregateErrorConstructor`, for typing these
+ error constructor parameters that handle the error constructor
+ polymorphically.
+ - The SES `console` now includes `error.cause` and `error.errors` in
+ its diagnostic output for errors.
diff --git a/packages/errors/index.js b/packages/errors/index.js
index 1a15cf0934..b3e3c27235 100644
--- a/packages/errors/index.js
+++ b/packages/errors/index.js
@@ -60,8 +60,8 @@ const {
} = globalAssert;
/** @type {import("ses").AssertionFunctions } */
// @ts-expect-error missing properties assigned next
-const assert = (value, optDetails, optErrorContructor) =>
- globalAssert(value, optDetails, optErrorContructor);
+const assert = (value, optDetails, errContructor, options) =>
+ globalAssert(value, optDetails, errContructor, options);
Object.assign(assert, assertions);
// As of 2024-02, the Agoric chain's bootstrap vat runs with a version of SES
diff --git a/packages/eslint-plugin/lib/configs/recommended.js b/packages/eslint-plugin/lib/configs/recommended.js
index 6f314dacfc..8733b0ab3b 100644
--- a/packages/eslint-plugin/lib/configs/recommended.js
+++ b/packages/eslint-plugin/lib/configs/recommended.js
@@ -61,6 +61,8 @@ module.exports = {
lockdown: 'readonly',
harden: 'readonly',
HandledPromise: 'readonly',
+ // https://github.com/endojs/endo/issues/550
+ AggregateError: 'readonly',
},
rules: {
'@endo/assert-fail-as-throw': 'error',
diff --git a/packages/marshal/NEWS.md b/packages/marshal/NEWS.md
index 67140ad2bc..bcf9f35117 100644
--- a/packages/marshal/NEWS.md
+++ b/packages/marshal/NEWS.md
@@ -1,5 +1,31 @@
User-visible changes in `@endo/marshal`:
+# next release
+
+- Sending and receiving extended errors.
+ - As of the previous release, `@endo/marshal` tolerates extra error
+ properties with `Passable` values. However, all those extra properties
+ were only recorded in annotations, since they are not recognized as
+ legitimate on `Passable` errors.
+ - This release will use these extra properties to construct an error object
+ with all those extra properties, and then call `toPassableError` to make
+ the locally `Passable` error that it returns. Thus, if the extra properties
+ received are not recognized as a legitimate part of a locally `Passable`
+ error, the error with those extra properties itself becomes the annotation
+ on the returned `Passable` error.
+ - An `error.cause` property whose value is a `Passable` error with therefore
+ show up on the returned `Passable` error. If it is any other `Passable`
+ value, it will show up on the internal error used to annotate the
+ returned error.
+ - An `error.errors` property whose value is a `CopyArray` of `Passable`
+ errors will likewise show up on the returned `Passable` error. Otherwise,
+ only on the internal error annotation of the returned error.
+ - Although this release does otherwise support the error properties
+ `error.cause` and `error.errors` on `Passable` errors, it still does not
+ send these properties because releases prior to the previous release
+ do not tolerate receiving them. Once we no longer need to support
+ releases prior to the previous release, then we can start sending these.
+
# v1.2.0 (2024-02-14)
- Tolerates receiving extra error properties (https://github.com/endojs/endo/pull/2052). Once pervasive, this tolerance will eventually enable additional error properties to be sent. The motivating examples are the JavaScript standard properties `cause` and `errors`. This change also enables smoother interoperation with other languages with their own theories about diagnostic information to be included in errors.
diff --git a/packages/marshal/src/marshal-justin.js b/packages/marshal/src/marshal-justin.js
index 69bd2d7ed6..4cb8adcad6 100644
--- a/packages/marshal/src/marshal-justin.js
+++ b/packages/marshal/src/marshal-justin.js
@@ -217,8 +217,9 @@ const decodeToJustin = (encoding, shouldIndent = false, slots = []) => {
}
case 'error': {
const { name, message } = rawTree;
- typeof name === 'string' ||
- Fail`invalid error name typeof ${q(typeof name)}`;
+ if (typeof name !== 'string') {
+ throw Fail`invalid error name typeof ${q(typeof name)}`;
+ }
getErrorConstructor(name) !== undefined ||
Fail`Must be the name of an Error constructor ${name}`;
typeof message === 'string' ||
@@ -389,11 +390,18 @@ const decodeToJustin = (encoding, shouldIndent = false, slots = []) => {
}
case 'error': {
- const { name, message } = rawTree;
- // TODO cause, errors, AggregateError
- // See https://github.com/endojs/endo/pull/2052
+ const {
+ name,
+ message,
+ cause = undefined,
+ errors = undefined,
+ } = rawTree;
+ cause === undefined ||
+ Fail`error cause not yet implemented in marshal-justin`;
name !== `AggregateError` ||
Fail`AggregateError not yet implemented in marshal-justin`;
+ errors === undefined ||
+ Fail`error errors not yet implemented in marshal-justin`;
return out.next(`${name}(${quote(message)})`);
}
diff --git a/packages/marshal/src/marshal.js b/packages/marshal/src/marshal.js
index edc8bb1d2c..d57e90c377 100644
--- a/packages/marshal/src/marshal.js
+++ b/packages/marshal/src/marshal.js
@@ -6,6 +6,7 @@ import {
getInterfaceOf,
getErrorConstructor,
hasOwnPropertyOf,
+ toPassableError,
} from '@endo/pass-style';
import { X, Fail, q, makeError, annotateError } from '@endo/errors';
@@ -30,6 +31,7 @@ import {
/** @typedef {import('./types.js').Encoding} Encoding */
/** @typedef {import('@endo/pass-style').RemotableObject} Remotable */
+const { defineProperties } = Object;
const { isArray } = Array;
const { ownKeys } = Reflect;
@@ -113,8 +115,9 @@ export const makeMarshal = (
assert.typeof(message, 'string');
const name = encodeRecur(`${err.name}`);
assert.typeof(name, 'string');
- // Must encode `cause`, `errors`.
- // nested non-passable errors must be ok from here.
+ // TODO Must encode `cause`, `errors`, but
+ // only once all possible counterparty decoders are tolerant of
+ // receiving them.
if (errorTagging === 'on') {
// We deliberately do not share the stack, but it would
// be useful to log the stack locally so someone who has
@@ -255,40 +258,65 @@ export const makeMarshal = (
};
/**
- * @param {{errorId?: string, message: string, name: string}} errData
+ * @param {{
+ * errorId?: string,
+ * message: string,
+ * name: string,
+ * cause: unknown,
+ * errors: unknown,
+ * }} errData
* @param {(e: unknown) => Passable} decodeRecur
* @returns {Error}
*/
const decodeErrorCommon = (errData, decodeRecur) => {
- const { errorId = undefined, message, name, ...rest } = errData;
- // TODO Must decode `cause` and `errors` properties.
+ const {
+ errorId = undefined,
+ message,
+ name,
+ cause = undefined,
+ errors = undefined,
+ ...rest
+ } = errData;
// See https://github.com/endojs/endo/pull/2052
// capData does not transform strings. The immediately following calls
// to `decodeRecur` are for reuse by other encodings that do,
// such as smallcaps.
const dName = decodeRecur(name);
const dMessage = decodeRecur(message);
+ // errorId is a late addition so be tolerant of its absence.
const dErrorId = errorId && decodeRecur(errorId);
typeof dName === 'string' ||
Fail`invalid error name typeof ${q(typeof dName)}`;
typeof dMessage === 'string' ||
Fail`invalid error message typeof ${q(typeof dMessage)}`;
- const EC = getErrorConstructor(dName) || Error;
- // errorId is a late addition so be tolerant of its absence.
+ const errConstructor = getErrorConstructor(dName) || Error;
const errorName =
dErrorId === undefined
- ? `Remote${EC.name}`
- : `Remote${EC.name}(${dErrorId})`;
- const error = makeError(dMessage, EC, { errorName });
- if (ownKeys(rest).length >= 1) {
- // Note that this does not decodeRecur rest's property names.
- // This would be inconsistent with smallcaps' expected handling,
- // but is fine here since it is only used for `annotateError`,
- // which is for diagnostic info that is otherwise unobservable.
- const extras = objectMap(rest, decodeRecur);
- annotateError(error, X`extra marshalled properties ${extras}`);
+ ? `Remote${errConstructor.name}`
+ : `Remote${errConstructor.name}(${dErrorId})`;
+ const options = {
+ errorName,
+ };
+ if (cause) {
+ options.cause = decodeRecur(cause);
+ }
+ if (errors) {
+ options.errors = decodeRecur(errors);
}
- return harden(error);
+ const rawError = makeError(dMessage, errConstructor, options);
+ // Note that this does not decodeRecur rest's property names.
+ // This would be inconsistent with smallcaps' expected handling,
+ // but is fine here since it is only used for `annotateError`,
+ // which is for diagnostic info that is otherwise unobservable.
+ const descs = objectMap(rest, data => ({
+ value: decodeRecur(data),
+ writable: false,
+ enumerable: false,
+ configurable: false,
+ }));
+ defineProperties(rawError, descs);
+ harden(rawError);
+ return toPassableError(rawError);
};
// The current encoding does not give the decoder enough into to distinguish
diff --git a/packages/marshal/src/types.js b/packages/marshal/src/types.js
index bc4899e1f5..d1d42ccfd9 100644
--- a/packages/marshal/src/types.js
+++ b/packages/marshal/src/types.js
@@ -31,7 +31,9 @@ export {};
* EncodingClass<'symbol'> & { name: string } |
* EncodingClass<'error'> & { name: string,
* message: string,
- * errorId?: string
+ * errorId?: string,
+ * cause?: Encoding,
+ * errors?: Encoding[],
* } |
* EncodingClass<'slot'> & { index: number,
* iface?: string
diff --git a/packages/marshal/test/test-marshal-capdata.js b/packages/marshal/test/test-marshal-capdata.js
index 503cb64749..e0251a97e8 100644
--- a/packages/marshal/test/test-marshal-capdata.js
+++ b/packages/marshal/test/test-marshal-capdata.js
@@ -172,10 +172,10 @@ test('unserialize extended errors', t => {
const aggErr = uns(
'{"@qclass":"error","message":"msg","name":"AggregateError","extraProp":"foo","cause":"bar","errors":["zip","zap"]}',
);
- t.is(getPrototypeOf(aggErr), Error.prototype); // direct instance of
+ t.is(getPrototypeOf(aggErr), AggregateError.prototype); // direct instance of
t.false('extraProp' in aggErr);
t.false('cause' in aggErr);
- t.false('errors' in aggErr);
+ t.is(aggErr.errors.length, 0);
console.log('error with extra prop', aggErr);
const unkErr = uns(
@@ -188,6 +188,41 @@ test('unserialize extended errors', t => {
console.log('error with extra prop', unkErr);
});
+test('unserialize errors w recognized extensions', t => {
+ const { unserialize } = makeTestMarshal();
+ const uns = body => unserialize({ body, slots: [] });
+
+ const errEnc = '{"@qclass":"error","message":"msg","name":"URIError"}';
+
+ const refErr = uns(
+ `{"@qclass":"error","message":"msg","name":"ReferenceError","extraProp":"foo","cause":${errEnc},"errors":[${errEnc}]}`,
+ );
+ t.is(getPrototypeOf(refErr), ReferenceError.prototype); // direct instance of
+ t.false('extraProp' in refErr);
+ t.is(getPrototypeOf(refErr.cause), URIError.prototype);
+ t.is(getPrototypeOf(refErr.errors[0]), URIError.prototype);
+ console.log('error with extra prop', refErr);
+
+ const aggErr = uns(
+ `{"@qclass":"error","message":"msg","name":"AggregateError","extraProp":"foo","cause":${errEnc},"errors":[${errEnc}]}`,
+ );
+ t.is(getPrototypeOf(aggErr), AggregateError.prototype); // direct instance of
+ t.false('extraProp' in aggErr);
+ t.is(getPrototypeOf(aggErr.cause), URIError.prototype);
+ t.is(getPrototypeOf(aggErr.errors[0]), URIError.prototype);
+ console.log('error with extra prop', aggErr);
+
+ const unkErr = uns(
+ `{"@qclass":"error","message":"msg","name":"UnknownError","extraProp":"foo","cause":${errEnc},"errors":[${errEnc}]}`,
+ );
+ t.is(getPrototypeOf(unkErr), Error.prototype); // direct instance of
+ t.false('extraProp' in unkErr);
+ t.is(getPrototypeOf(unkErr.cause), URIError.prototype);
+ t.is(getPrototypeOf(unkErr.errors[0]), URIError.prototype);
+
+ console.log('error with extra prop', unkErr);
+});
+
test('passStyleOf null is "null"', t => {
t.assert(passStyleOf(null), 'null');
});
diff --git a/packages/marshal/test/test-marshal-smallcaps.js b/packages/marshal/test/test-marshal-smallcaps.js
index 86fe5e75b2..9b285dbe02 100644
--- a/packages/marshal/test/test-marshal-smallcaps.js
+++ b/packages/marshal/test/test-marshal-smallcaps.js
@@ -163,9 +163,6 @@ test('smallcaps unserialize extended errors', t => {
const { unserialize } = makeTestMarshal();
const uns = body => unserialize({ body, slots: [] });
- // TODO cause, errors, and AggregateError will eventually be recognized.
- // See https://github.com/endojs/endo/pull/2042
-
const refErr = uns(
'#{"#error":"msg","name":"ReferenceError","extraProp":"foo","cause":"bar","errors":["zip","zap"]}',
);
@@ -178,10 +175,10 @@ test('smallcaps unserialize extended errors', t => {
const aggErr = uns(
'#{"#error":"msg","name":"AggregateError","extraProp":"foo","cause":"bar","errors":["zip","zap"]}',
);
- t.is(getPrototypeOf(aggErr), Error.prototype); // direct instance of
+ t.is(getPrototypeOf(aggErr), AggregateError.prototype); // direct instance of
t.false('extraProp' in aggErr);
t.false('cause' in aggErr);
- t.false('errors' in aggErr);
+ t.is(aggErr.errors.length, 0);
console.log('error with extra prop', aggErr);
const unkErr = uns(
@@ -194,6 +191,40 @@ test('smallcaps unserialize extended errors', t => {
console.log('error with extra prop', unkErr);
});
+test('smallcaps unserialize errors w recognized extensions', t => {
+ const { unserialize } = makeTestMarshal();
+ const uns = body => unserialize({ body, slots: [] });
+
+ const errEnc = '{"#error":"msg","name":"URIError"}';
+
+ const refErr = uns(
+ `#{"#error":"msg","name":"ReferenceError","extraProp":"foo","cause":${errEnc},"errors":[${errEnc}]}`,
+ );
+ t.is(getPrototypeOf(refErr), ReferenceError.prototype); // direct instance of
+ t.false('extraProp' in refErr);
+ t.is(getPrototypeOf(refErr.cause), URIError.prototype);
+ t.is(getPrototypeOf(refErr.errors[0]), URIError.prototype);
+ console.log('error with extra prop', refErr);
+
+ const aggErr = uns(
+ `#{"#error":"msg","name":"AggregateError","extraProp":"foo","cause":${errEnc},"errors":[${errEnc}]}`,
+ );
+ t.is(getPrototypeOf(aggErr), AggregateError.prototype); // direct instance of
+ t.false('extraProp' in aggErr);
+ t.is(getPrototypeOf(refErr.cause), URIError.prototype);
+ t.is(getPrototypeOf(refErr.errors[0]), URIError.prototype);
+ console.log('error with extra prop', aggErr);
+
+ const unkErr = uns(
+ `#{"#error":"msg","name":"UnknownError","extraProp":"foo","cause":${errEnc},"errors":[${errEnc}]}`,
+ );
+ t.is(getPrototypeOf(unkErr), Error.prototype); // direct instance of
+ t.false('extraProp' in unkErr);
+ t.is(getPrototypeOf(refErr.cause), URIError.prototype);
+ t.is(getPrototypeOf(refErr.errors[0]), URIError.prototype);
+ console.log('error with extra prop', unkErr);
+});
+
test('smallcaps mal-formed @qclass', t => {
const { unserialize } = makeTestMarshal();
const uns = body => unserialize({ body, slots: [] });
@@ -396,7 +427,7 @@ test('smallcaps encoding examples', t => {
harden(nonPassableErr);
t.throws(() => passStyleOf(nonPassableErr), {
message:
- /Passed Error has extra unpassed properties {"extraProperty":{"configurable":.*,"enumerable":true,"value":"something bad","writable":.*}}/,
+ /Passable Error "extraProperty" own property must not be enumerable: \{"configurable":.*,"enumerable":true,"value":"something bad","writable":.*\}/,
});
assertSer(
nonPassableErr,
diff --git a/packages/pass-style/NEWS.md b/packages/pass-style/NEWS.md
index e69de29bb2..767cc5cc17 100644
--- a/packages/pass-style/NEWS.md
+++ b/packages/pass-style/NEWS.md
@@ -0,0 +1,17 @@
+User-visible changes in `@endo/pass-style`:
+
+# next release
+
+- Now supports `AggegateError`, `error.errors`, `error.cause`.
+ - A `Passable` error can now include an `error.cause` property whose
+ value is a `Passable` error.
+ - An `AggregateError` can be a `Passable` error.
+ - A `Passable` error can now include an `error.errors` property whose
+ value is a `CopyArray` of `Passable` errors.
+ - The previously internal `toPassableError` is more general and exported
+ for general use. If its error agument is already `Passable`,
+ `toPassableError` will return it. Otherwise, it will extract from it
+ info for making a `Passable` error, and use `annotateError` to attach
+ the original error to the returned `Passable` error as a note. This
+ node will show up on the SES `console` as additional diagnostic info
+ associated with the returned `Passable` error.
diff --git a/packages/pass-style/index.js b/packages/pass-style/index.js
index da985131a2..32e6d7562d 100644
--- a/packages/pass-style/index.js
+++ b/packages/pass-style/index.js
@@ -7,11 +7,8 @@ export {
hasOwnPropertyOf,
} from './src/passStyle-helpers.js';
-export {
- getErrorConstructor,
- toPassableError,
- isErrorLike,
-} from './src/error.js';
+export { getErrorConstructor, isErrorLike } from './src/error.js';
+
export { getInterfaceOf } from './src/remotable.js';
export {
@@ -21,7 +18,14 @@ export {
passableSymbolForName,
} from './src/symbol.js';
-export { passStyleOf, assertPassable } from './src/passStyleOf.js';
+export {
+ passStyleOf,
+ isPassable,
+ assertPassable,
+ isPassableError,
+ assertPassableError,
+ toPassableError,
+} from './src/passStyleOf.js';
export { makeTagged } from './src/makeTagged.js';
export {
diff --git a/packages/pass-style/src/error.js b/packages/pass-style/src/error.js
index d015aaac9b..dc9d4afae5 100644
--- a/packages/pass-style/src/error.js
+++ b/packages/pass-style/src/error.js
@@ -1,27 +1,43 @@
///
-import { X, Fail, annotateError } from '@endo/errors';
+import { X, q } from '@endo/errors';
import { assertChecker } from './passStyle-helpers.js';
/** @typedef {import('./internal-types.js').PassStyleHelper} PassStyleHelper */
/** @typedef {import('./types.js').Checker} Checker */
-const { getPrototypeOf, getOwnPropertyDescriptors } = Object;
-const { ownKeys } = Reflect;
+const { getPrototypeOf, getOwnPropertyDescriptors, hasOwn, entries } = Object;
// TODO: Maintenance hazard: Coordinate with the list of errors in the SES
-// whilelist. Currently, both omit AggregateError, which is now standard. Both
-// must eventually include it.
-const errorConstructors = new Map([
- ['Error', Error],
- ['EvalError', EvalError],
- ['RangeError', RangeError],
- ['ReferenceError', ReferenceError],
- ['SyntaxError', SyntaxError],
- ['TypeError', TypeError],
- ['URIError', URIError],
-]);
+// whilelist.
+const errorConstructors = new Map(
+ // Cast because otherwise TS is confused by AggregateError
+ // See https://github.com/endojs/endo/pull/2042#discussion_r1484933028
+ /** @type {Array<[string, import('ses').GenericErrorConstructor]>} */
+ ([
+ ['Error', Error],
+ ['EvalError', EvalError],
+ ['RangeError', RangeError],
+ ['ReferenceError', ReferenceError],
+ ['SyntaxError', SyntaxError],
+ ['TypeError', TypeError],
+ ['URIError', URIError],
+ // https://github.com/endojs/endo/issues/550
+ ['AggregateError', AggregateError],
+ ]),
+);
+
+/**
+ * Because the error constructor returned by this function might be
+ * `AggregateError`, which has different construction parameters
+ * from the other error constructors, do not use it directly to try
+ * to make an error instance. Rather, use `makeError` which encapsulates
+ * this non-uniformity.
+ *
+ * @param {string} name
+ * @returns {import('ses').GenericErrorConstructor | undefined}
+ */
export const getErrorConstructor = name => errorConstructors.get(name);
harden(getErrorConstructor);
@@ -39,6 +55,7 @@ const checkErrorLike = (candidate, check = undefined) => {
);
};
harden(checkErrorLike);
+///
/**
* Validating error objects are passable raises a tension between security
@@ -62,61 +79,139 @@ export const isErrorLike = candidate => checkErrorLike(candidate);
harden(isErrorLike);
/**
- * @type {PassStyleHelper}
+ * @param {string} propName
+ * @param {PropertyDescriptor} desc
+ * @param {import('./internal-types.js').PassStyleOf} passStyleOfRecur
+ * @param {Checker} [check]
+ * @returns {boolean}
*/
-export const ErrorHelper = harden({
- styleName: 'error',
-
- canBeValid: checkErrorLike,
-
- assertValid: candidate => {
- ErrorHelper.canBeValid(candidate, assertChecker);
- const proto = getPrototypeOf(candidate);
- const { name } = proto;
- const EC = getErrorConstructor(name);
- (EC && EC.prototype === proto) ||
- Fail`Errors must inherit from an error class .prototype ${candidate}`;
-
- const {
- // TODO Must allow `cause`, `errors`
- message: mDesc,
- stack: stackDesc,
- ...restDescs
- } = getOwnPropertyDescriptors(candidate);
- ownKeys(restDescs).length < 1 ||
- Fail`Passed Error has extra unpassed properties ${restDescs}`;
- if (mDesc) {
- typeof mDesc.value === 'string' ||
- Fail`Passed Error "message" ${mDesc} must be a string-valued data property.`;
- !mDesc.enumerable ||
- Fail`Passed Error "message" ${mDesc} must not be enumerable`;
+export const checkRecursivelyPassableErrorPropertyDesc = (
+ propName,
+ desc,
+ passStyleOfRecur,
+ check = undefined,
+) => {
+ const reject = !!check && (details => check(false, details));
+ if (desc.enumerable) {
+ return (
+ reject &&
+ reject(
+ X`Passable Error ${q(
+ propName,
+ )} own property must not be enumerable: ${desc}`,
+ )
+ );
+ }
+ if (!hasOwn(desc, 'value')) {
+ return (
+ reject &&
+ reject(
+ X`Passable Error ${q(
+ propName,
+ )} own property must be a data property: ${desc}`,
+ )
+ );
+ }
+ const { value } = desc;
+ switch (propName) {
+ case 'message':
+ case 'stack': {
+ return (
+ typeof value === 'string' ||
+ (reject &&
+ reject(
+ X`Passable Error ${q(
+ propName,
+ )} own property must be a string: ${value}`,
+ ))
+ );
}
- if (stackDesc) {
- typeof stackDesc.value === 'string' ||
- Fail`Passed Error "stack" ${stackDesc} must be a string-valued data property.`;
- !stackDesc.enumerable ||
- Fail`Passed Error "stack" ${stackDesc} must not be enumerable`;
+ case 'cause': {
+ // eslint-disable-next-line no-use-before-define
+ return checkRecursivelyPassableError(value, passStyleOfRecur, check);
}
- return true;
- },
-});
+ case 'errors': {
+ if (!Array.isArray(value) || passStyleOfRecur(value) !== 'copyArray') {
+ return (
+ reject &&
+ reject(
+ X`Passable Error ${q(
+ propName,
+ )} own property must be a copyArray: ${value}`,
+ )
+ );
+ }
+ return value.every(err =>
+ // eslint-disable-next-line no-use-before-define
+ checkRecursivelyPassableError(err, passStyleOfRecur, check),
+ );
+ }
+ default: {
+ break;
+ }
+ }
+ return (
+ reject &&
+ reject(X`Passable Error has extra unpassed property ${q(propName)}`)
+ );
+};
+harden(checkRecursivelyPassableErrorPropertyDesc);
/**
- * Return a new passable error that propagates the diagnostic info of the
- * original, and is linked to the original as a note.
- *
- * @param {Error} err
- * @returns {Error}
+ * @param {unknown} candidate
+ * @param {import('./internal-types.js').PassStyleOf} passStyleOfRecur
+ * @param {Checker} [check]
+ * @returns {boolean}
*/
-export const toPassableError = err => {
- const { name, message } = err;
+export const checkRecursivelyPassableError = (
+ candidate,
+ passStyleOfRecur,
+ check = undefined,
+) => {
+ const reject = !!check && (details => check(false, details));
+ if (!checkErrorLike(candidate, check)) {
+ return false;
+ }
+ const proto = getPrototypeOf(candidate);
+ const { name } = proto;
+ const errConstructor = getErrorConstructor(name);
+ if (errConstructor === undefined || errConstructor.prototype !== proto) {
+ return (
+ reject &&
+ reject(
+ X`Passable Error must inherit from an error class .prototype: ${candidate}`,
+ )
+ );
+ }
+ const descs = getOwnPropertyDescriptors(candidate);
+ if (!('message' in descs)) {
+ return (
+ reject &&
+ reject(
+ X`Passable Error must have an own "message" string property: ${candidate}`,
+ )
+ );
+ }
- const EC = getErrorConstructor(`${name}`) || Error;
- const newError = harden(new EC(`${message}`));
- // Even the cleaned up error copy, if sent to the console, should
- // cause hidden diagnostic information of the original error
- // to be logged.
- annotateError(newError, X`copied from error ${err}`);
- return newError;
+ return entries(descs).every(([propName, desc]) =>
+ checkRecursivelyPassableErrorPropertyDesc(
+ propName,
+ desc,
+ passStyleOfRecur,
+ check,
+ ),
+ );
};
-harden(toPassableError);
+harden(checkRecursivelyPassableError);
+
+/**
+ * @type {PassStyleHelper}
+ */
+export const ErrorHelper = harden({
+ styleName: 'error',
+
+ canBeValid: checkErrorLike,
+
+ assertValid: (candidate, passStyleOfRecur) =>
+ checkRecursivelyPassableError(candidate, passStyleOfRecur, assertChecker),
+});
diff --git a/packages/pass-style/src/passStyleOf.js b/packages/pass-style/src/passStyleOf.js
index a335b2bca2..1f2ddb05a8 100644
--- a/packages/pass-style/src/passStyleOf.js
+++ b/packages/pass-style/src/passStyleOf.js
@@ -3,13 +3,23 @@
///
import { isPromise } from '@endo/promise-kit';
-import { X, Fail, q } from '@endo/errors';
-import { isObject, isTypedArray, PASS_STYLE } from './passStyle-helpers.js';
+import { X, Fail, q, annotateError, makeError } from '@endo/errors';
+import {
+ assertChecker,
+ isObject,
+ isTypedArray,
+ PASS_STYLE,
+} from './passStyle-helpers.js';
import { CopyArrayHelper } from './copyArray.js';
import { CopyRecordHelper } from './copyRecord.js';
import { TaggedHelper } from './tagged.js';
-import { ErrorHelper } from './error.js';
+import {
+ ErrorHelper,
+ checkRecursivelyPassableErrorPropertyDesc,
+ checkRecursivelyPassableError,
+ getErrorConstructor,
+} from './error.js';
import { RemotableHelper } from './remotable.js';
import { assertPassableSymbol } from './symbol.js';
@@ -24,7 +34,7 @@ import { assertSafePromise } from './safe-promise.js';
/** @typedef {Exclude} HelperPassStyle */
const { ownKeys } = Reflect;
-const { isFrozen } = Object;
+const { isFrozen, getOwnPropertyDescriptors } = Object;
/**
* @param {PassStyleHelper[]} passStyleHelpers
@@ -221,3 +231,107 @@ export const assertPassable = val => {
passStyleOf(val); // throws if val is not a passable
};
harden(assertPassable);
+
+/**
+ * Is `specimen` Passable? This returns true iff `passStyleOf(specimen)`
+ * returns a string. This returns `false` iff `passStyleOf(specimen)` throws.
+ * Under no normal circumstance should `isPassable(specimen)` throw.
+ *
+ * TODO Deprecate and ultimately delete @agoric/base-zone's `isPassable' in
+ * favor of this one.
+ *
+ * TODO implement an isPassable that does not rely on try/catch.
+ * This implementation is just a standin until then
+ *
+ * @param {any} specimen
+ * @returns {specimen is Passable}
+ */
+export const isPassable = specimen => {
+ try {
+ // In fact, it never returns undefined. It either returns a
+ // string or throws.
+ return passStyleOf(specimen) !== undefined;
+ } catch (_) {
+ return false;
+ }
+};
+harden(isPassable);
+
+/**
+ * @param {string} name
+ * @param {PropertyDescriptor} desc
+ * @returns {boolean}
+ */
+const isPassableErrorPropertyDesc = (name, desc) =>
+ checkRecursivelyPassableErrorPropertyDesc(name, desc, passStyleOf);
+harden(isPassableErrorPropertyDesc);
+
+/**
+ * @param {string} name
+ * @param {PropertyDescriptor} desc
+ */
+const assertPassableErrorPropertyDesc = (name, desc) => {
+ checkRecursivelyPassableErrorPropertyDesc(
+ name,
+ desc,
+ passStyleOf,
+ assertChecker,
+ );
+};
+harden(assertPassableErrorPropertyDesc);
+
+/**
+ * @param {unknown} err
+ * @returns {err is Error}
+ */
+export const isPassableError = err =>
+ checkRecursivelyPassableError(err, passStyleOf);
+
+/**
+ * @param {unknown} err
+ * @returns {asserts err is Error}
+ */
+export const assertPassableError = err => {
+ checkRecursivelyPassableError(err, passStyleOf, assertChecker);
+};
+
+/**
+ * Return a new passable error that propagates the diagnostic info of the
+ * original, and is linked to the original as a note.
+ *
+ * @param {Error | AggregateError} err
+ * @returns {Error}
+ */
+export const toPassableError = err => {
+ if (isPassableError(err)) {
+ return err;
+ }
+ const { name, message } = err;
+ const { cause: causeDesc, errors: errorsDesc } =
+ getOwnPropertyDescriptors(err);
+ let cause;
+ let errors;
+ if (causeDesc && isPassableErrorPropertyDesc('cause', causeDesc)) {
+ // @ts-expect-error data descriptors have "value" property
+ cause = causeDesc.value;
+ }
+ if (errorsDesc && isPassableErrorPropertyDesc('errors', errorsDesc)) {
+ // @ts-expect-error data descriptors have "value" property
+ errors = errorsDesc.value;
+ }
+
+ const errConstructor = getErrorConstructor(`${name}`) || Error;
+ const newError = makeError(`${message}`, errConstructor, {
+ // @ts-ignore Assuming cause is Error | undefined
+ cause,
+ errors,
+ });
+ harden(newError);
+ // Even the cleaned up error copy, if sent to the console, should
+ // cause hidden diagnostic information of the original error
+ // to be logged.
+ annotateError(newError, X`copied from error ${err}`);
+ assertPassableError(newError);
+ return newError;
+};
+harden(toPassableError);
diff --git a/packages/pass-style/test/test-extended-errors.js b/packages/pass-style/test/test-extended-errors.js
new file mode 100644
index 0000000000..0f653bf39a
--- /dev/null
+++ b/packages/pass-style/test/test-extended-errors.js
@@ -0,0 +1,23 @@
+/* eslint-disable max-classes-per-file */
+import { test } from './prepare-test-env-ava.js';
+
+// eslint-disable-next-line import/order
+import { passStyleOf } from '../src/passStyleOf.js';
+
+test('style of extended errors', t => {
+ const e1 = Error('e1');
+ t.throws(() => passStyleOf(e1), {
+ message: 'Cannot pass non-frozen objects like "[Error: e1]". Use harden()',
+ });
+ harden(e1);
+ t.is(passStyleOf(e1), 'error');
+
+ const e2 = harden(Error('e2', { cause: e1 }));
+ t.is(passStyleOf(e2), 'error');
+
+ const u3 = harden(URIError('u3', { cause: e1 }));
+ t.is(passStyleOf(u3), 'error');
+
+ const a4 = harden(AggregateError([e2, u3], 'a4', { cause: e1 }));
+ t.is(passStyleOf(a4), 'error');
+});
diff --git a/packages/pass-style/test/test-passStyleOf.js b/packages/pass-style/test/test-passStyleOf.js
index 33c71a1bd9..0560223e55 100644
--- a/packages/pass-style/test/test-passStyleOf.js
+++ b/packages/pass-style/test/test-passStyleOf.js
@@ -30,15 +30,18 @@ test('passStyleOf basic success cases', t => {
t.is(passStyleOf(harden({})), 'copyRecord', 'empty plain object');
t.is(passStyleOf(makeTagged('unknown', undefined)), 'tagged');
t.is(passStyleOf(harden(Error('ok'))), 'error');
+});
+test('some passStyleOf rejections', t => {
const hairlessError = Error('hairless');
for (const k of ownKeys(hairlessError)) {
delete hairlessError[k];
}
- t.is(passStyleOf(harden(hairlessError)), 'error');
-});
+ t.throws(() => passStyleOf(harden(hairlessError)), {
+ message:
+ 'Passable Error must have an own "message" string property: "[Error: ]"',
+ });
-test('some passStyleOf rejections', t => {
t.throws(() => passStyleOf(Symbol('unique')), {
message:
/Only registered symbols or well-known symbols are passable: "\[Symbol\(unique\)\]"/,
@@ -391,7 +394,7 @@ test('remotables - safety from the gibson042 attack', t => {
// console.log(passStyleOf(input1)); // => "remotable"
t.throws(() => passStyleOf(input1), {
message:
- 'Errors must inherit from an error class .prototype "[undefined: undefined]"',
+ 'Passable Error must inherit from an error class .prototype: "[undefined: undefined]"',
});
// different because of changes in the prototype
@@ -400,7 +403,7 @@ test('remotables - safety from the gibson042 attack', t => {
// console.log(passStyleOf(input2)); // => Error (Errors must inherit from an error class .prototype)
t.throws(() => passStyleOf(input2), {
message:
- 'Errors must inherit from an error class .prototype "[undefined: undefined]"',
+ 'Passable Error must inherit from an error class .prototype: "[undefined: undefined]"',
});
});
@@ -417,8 +420,7 @@ test('Unexpected stack on errors', t => {
Object.freeze(err);
t.throws(() => passStyleOf(err), {
- message:
- 'Passed Error "stack" {"configurable":false,"enumerable":false,"value":{},"writable":false} must be a string-valued data property.',
+ message: 'Passable Error "stack" own property must be a string: {}',
});
err.stack.foo = 42;
});
diff --git a/packages/pass-style/tsconfig.json b/packages/pass-style/tsconfig.json
index f77b8008a1..20335e4343 100644
--- a/packages/pass-style/tsconfig.json
+++ b/packages/pass-style/tsconfig.json
@@ -1,5 +1,9 @@
{
"extends": "../../tsconfig.eslint-base.json",
+ "compilerOptions": {
+ "checkJs": true,
+ "maxNodeModuleJsDepth": 1,
+ },
"include": [
"*.js",
"*.ts",
diff --git a/packages/ses/NEWS.md b/packages/ses/NEWS.md
index e9e5221c69..b35dca6a43 100644
--- a/packages/ses/NEWS.md
+++ b/packages/ses/NEWS.md
@@ -1,5 +1,25 @@
User-visible changes in SES:
+# next release
+
+- Now supports `Promise.any`, `AggegateError`, `error.errors`,
+ and `error.cause`.
+ - Assertion functions/methods that were parameterized by an error constructor
+ (`makeError`, `assert`, `assert.fail`, `assert.equal`) now also accept named
+ options `cause` and `errors` in their immediately succeeding
+ `options` argument.
+ - For all those, the error constructor can now be an `AggregateError`.
+ If they do make an error instance, they encapsulate the
+ non-uniformity of the `AggregateError` construction arguments, allowing
+ all the error constructors to be used polymorphically
+ (generic / interchangeable).
+ - Adds a `GenericErrorConstructor` type to be effectively the common supertype
+ of `ErrorConstructor` and `AggregateErrorConstructor`, for typing these
+ error constructor parameters that handle the error constructor
+ polymorphically.
+ - The SES `console` now includes `error.cause` and `error.errors` in
+ its diagnostic output for errors.
+
# v1.2.0 (2024-02-14)
- Exports `ses/lockdown-shim.js`, `ses/compartment-shim.js`, and
diff --git a/packages/ses/package.json b/packages/ses/package.json
index 669ac6f969..979100b364 100644
--- a/packages/ses/package.json
+++ b/packages/ses/package.json
@@ -161,7 +161,8 @@
"isNaN",
"parseFloat",
"parseInt",
- "unescape"
+ "unescape",
+ "AggregateError"
],
"@endo/no-polymorphic-call": "error"
},
diff --git a/packages/ses/src/commons.js b/packages/ses/src/commons.js
index 2d53725267..2ef01d7425 100644
--- a/packages/ses/src/commons.js
+++ b/packages/ses/src/commons.js
@@ -47,6 +47,7 @@ export const {
ReferenceError,
SyntaxError,
TypeError,
+ AggregateError,
} = globalThis;
export const {
diff --git a/packages/ses/src/enablements.js b/packages/ses/src/enablements.js
index 440398ef55..9124238aa7 100644
--- a/packages/ses/src/enablements.js
+++ b/packages/ses/src/enablements.js
@@ -149,6 +149,12 @@ export const moderateEnablements = {
name: true, // set by "node 14"
},
+ // https://github.com/endojs/endo/issues/550
+ '%AggregateErrorPrototype%': {
+ message: true, // to match TypeErrorPrototype.message
+ name: true, // set by "node 14"?
+ },
+
'%PromisePrototype%': {
constructor: true, // set by "core-js"
},
diff --git a/packages/ses/src/error/assert.js b/packages/ses/src/error/assert.js
index ada53abe86..6dae9e3ae4 100644
--- a/packages/ses/src/error/assert.js
+++ b/packages/ses/src/error/assert.js
@@ -21,6 +21,7 @@ import {
arrayPush,
assign,
freeze,
+ defineProperty,
globalThis,
is,
isError,
@@ -33,6 +34,7 @@ import {
weakmapGet,
weakmapHas,
weakmapSet,
+ AggregateError,
} from '../commons.js';
import { an, bestEffortStringify } from './stringify-utils.js';
import './types.js';
@@ -257,8 +259,8 @@ const tagError = (err, optErrorName = err.name) => {
*/
const makeError = (
optDetails = redactedDetails`Assert failed`,
- ErrorConstructor = globalThis.Error,
- { errorName = undefined } = {},
+ errConstructor = globalThis.Error,
+ { errorName = undefined, cause = undefined, errors = undefined } = {},
) => {
if (typeof optDetails === 'string') {
// If it is a string, use it as the literal part of the template so
@@ -270,7 +272,26 @@ const makeError = (
throw TypeError(`unrecognized details ${quote(optDetails)}`);
}
const messageString = getMessageString(hiddenDetails);
- const error = new ErrorConstructor(messageString);
+ const opts = cause && { cause };
+ let error;
+ if (errConstructor === AggregateError) {
+ error = AggregateError(errors || [], messageString, opts);
+ } else {
+ error = /** @type {ErrorConstructor} */ (errConstructor)(
+ messageString,
+ opts,
+ );
+ if (errors !== undefined) {
+ // Since we need to tolerate `errors` on an AggregateError, may as
+ // well tolerate it on all errors.
+ defineProperty(error, 'errors', {
+ value: errors,
+ writable: true,
+ enumerable: false,
+ configurable: true,
+ });
+ }
+ }
weakmapSet(hiddenMessageLogArgs, error, getLogArgs(hiddenDetails));
if (errorName !== undefined) {
tagError(error, errorName);
@@ -382,9 +403,10 @@ const makeAssert = (optRaise = undefined, unredacted = false) => {
/** @type {AssertFail} */
const fail = (
optDetails = assertFailedDetails,
- ErrorConstructor = globalThis.Error,
+ errConstructor = undefined,
+ options = undefined,
) => {
- const reason = makeError(optDetails, ErrorConstructor);
+ const reason = makeError(optDetails, errConstructor, options);
if (optRaise !== undefined) {
optRaise(reason);
}
@@ -402,9 +424,10 @@ const makeAssert = (optRaise = undefined, unredacted = false) => {
function baseAssert(
flag,
optDetails = undefined,
- ErrorConstructor = undefined,
+ errConstructor = undefined,
+ options = undefined,
) {
- flag || fail(optDetails, ErrorConstructor);
+ flag || fail(optDetails, errConstructor, options);
}
/** @type {AssertEqual} */
@@ -412,12 +435,14 @@ const makeAssert = (optRaise = undefined, unredacted = false) => {
actual,
expected,
optDetails = undefined,
- ErrorConstructor = undefined,
+ errConstructor = undefined,
+ options = undefined,
) => {
is(actual, expected) ||
fail(
optDetails || details`Expected ${actual} is same as ${expected}`,
- ErrorConstructor || RangeError,
+ errConstructor || RangeError,
+ options,
);
};
freeze(equal);
diff --git a/packages/ses/src/error/console.js b/packages/ses/src/error/console.js
index 0efd6fb15b..950bdd5c31 100644
--- a/packages/ses/src/error/console.js
+++ b/packages/ses/src/error/console.js
@@ -169,6 +169,8 @@ export { makeLoggingConsoleKit };
const ErrorInfo = {
NOTE: 'ERROR_NOTE:',
MESSAGE: 'ERROR_MESSAGE:',
+ CAUSE: 'cause:',
+ ERRORS: 'errors:',
};
freeze(ErrorInfo);
@@ -308,6 +310,14 @@ const makeCausalConsole = (baseConsole, loggedErrorHandler) => {
// eslint-disable-next-line @endo/no-polymorphic-call
baseConsole[severity](stackString);
// Show the other annotations on error
+ if (error.cause) {
+ logErrorInfo(severity, error, ErrorInfo.CAUSE, [error.cause], subErrors);
+ }
+ // @ts-expect-error AggregateError has an `errors` property.
+ if (error.errors) {
+ // @ts-expect-error AggregateError has an `errors` property.
+ logErrorInfo(severity, error, ErrorInfo.ERRORS, error.errors, subErrors);
+ }
for (const noteLogArgs of noteLogArgsArray) {
logErrorInfo(severity, error, ErrorInfo.NOTE, noteLogArgs, subErrors);
}
diff --git a/packages/ses/src/error/internal-types.js b/packages/ses/src/error/internal-types.js
index caca8cfbfe..9cfe79413b 100644
--- a/packages/ses/src/error/internal-types.js
+++ b/packages/ses/src/error/internal-types.js
@@ -69,7 +69,12 @@
*/
/**
- * @typedef {{ NOTE: 'ERROR_NOTE:', MESSAGE: 'ERROR_MESSAGE:' }} ErrorInfo
+ * @typedef {{
+ * NOTE: 'ERROR_NOTE:',
+ * MESSAGE: 'ERROR_MESSAGE:',
+ * CAUSE: 'cause:',
+ * ERRORS: 'errors:',
+ * }} ErrorInfo
*/
/**
diff --git a/packages/ses/src/error/types.js b/packages/ses/src/error/types.js
index 4038cc5168..d3ecdfaa52 100644
--- a/packages/ses/src/error/types.js
+++ b/packages/ses/src/error/types.js
@@ -1,19 +1,35 @@
// @ts-check
+/**
+ * TypeScript does not treat `AggregateErrorConstructor` as a subtype of
+ * `ErrorConstructor`, which makes sense because their constructors
+ * have incompatible signatures. However, we want to parameterize some
+ * operations by any error constructor, including possible `AggregateError`.
+ * So we introduce `GenericErrorConstructor` as a common supertype. Any call
+ * to it to make an instance must therefore first case split on whether the
+ * constructor is an AggregateErrorConstructor or a normal ErrorConstructor.
+ *
+ * @typedef {ErrorConstructor | AggregateErrorConstructor} GenericErrorConstructor
+ */
+
/**
* @callback BaseAssert
* The `assert` function itself.
*
* @param {any} flag The truthy/falsy value
- * @param {Details=} optDetails The details to throw
- * @param {ErrorConstructor=} ErrorConstructor An optional alternate error
- * constructor to use.
+ * @param {Details} [optDetails] The details to throw
+ * @param {GenericErrorConstructor} [errConstructor]
+ * An optional alternate error constructor to use.
+ * @param {AssertMakeErrorOptions} [options]
* @returns {asserts flag}
*/
/**
* @typedef {object} AssertMakeErrorOptions
- * @property {string=} errorName
+ * @property {string} [errorName]
+ * @property {Error} [cause]
+ * @property {Error[]} [errors]
+ * Normally only used when the ErrorConstuctor is `AggregateError`
*/
/**
@@ -22,10 +38,10 @@
* The `assert.error` method, recording details for the console.
*
* The optional `optDetails` can be a string.
- * @param {Details=} optDetails The details of what was asserted
- * @param {ErrorConstructor=} ErrorConstructor An optional alternate error
- * constructor to use.
- * @param {AssertMakeErrorOptions=} options
+ * @param {Details} [optDetails] The details of what was asserted
+ * @param {GenericErrorConstructor} [errConstructor]
+ * An optional alternate error constructor to use.
+ * @param {AssertMakeErrorOptions} [options]
* @returns {Error}
*/
@@ -40,9 +56,10 @@
*
* The optional `optDetails` can be a string for backwards compatibility
* with the nodejs assertion library.
- * @param {Details=} optDetails The details of what was asserted
- * @param {ErrorConstructor=} ErrorConstructor An optional alternate error
- * constructor to use.
+ * @param {Details} [optDetails] The details of what was asserted
+ * @param {GenericErrorConstructor} [errConstructor]
+ * An optional alternate error constructor to use.
+ * @param {AssertMakeErrorOptions} [options]
* @returns {never}
*/
@@ -53,9 +70,10 @@
* Assert that two values must be `Object.is`.
* @param {any} actual The value we received
* @param {any} expected What we wanted
- * @param {Details=} optDetails The details to throw
- * @param {ErrorConstructor=} ErrorConstructor An optional alternate error
- * constructor to use.
+ * @param {Details} [optDetails] The details to throw
+ * @param {GenericErrorConstructor} [errConstructor]
+ * An optional alternate error constructor to use.
+ * @param {AssertMakeErrorOptions} [options]
* @returns {void}
*/
@@ -66,7 +84,7 @@
* @callback AssertTypeofBigint
* @param {any} specimen
* @param {'bigint'} typename
- * @param {Details=} optDetails
+ * @param {Details} [optDetails]
* @returns {asserts specimen is bigint}
*/
@@ -74,7 +92,7 @@
* @callback AssertTypeofBoolean
* @param {any} specimen
* @param {'boolean'} typename
- * @param {Details=} optDetails
+ * @param {Details} [optDetails]
* @returns {asserts specimen is boolean}
*/
@@ -82,7 +100,7 @@
* @callback AssertTypeofFunction
* @param {any} specimen
* @param {'function'} typename
- * @param {Details=} optDetails
+ * @param {Details} [optDetails]
* @returns {asserts specimen is Function}
*/
@@ -90,7 +108,7 @@
* @callback AssertTypeofNumber
* @param {any} specimen
* @param {'number'} typename
- * @param {Details=} optDetails
+ * @param {Details} [optDetails]
* @returns {asserts specimen is number}
*/
@@ -98,7 +116,7 @@
* @callback AssertTypeofObject
* @param {any} specimen
* @param {'object'} typename
- * @param {Details=} optDetails
+ * @param {Details} [optDetails]
* @returns {asserts specimen is Record | null}
*/
@@ -106,7 +124,7 @@
* @callback AssertTypeofString
* @param {any} specimen
* @param {'string'} typename
- * @param {Details=} optDetails
+ * @param {Details} [optDetails]
* @returns {asserts specimen is string}
*/
@@ -114,7 +132,7 @@
* @callback AssertTypeofSymbol
* @param {any} specimen
* @param {'symbol'} typename
- * @param {Details=} optDetails
+ * @param {Details} [optDetails]
* @returns {asserts specimen is symbol}
*/
@@ -122,7 +140,7 @@
* @callback AssertTypeofUndefined
* @param {any} specimen
* @param {'undefined'} typename
- * @param {Details=} optDetails
+ * @param {Details} [optDetails]
* @returns {asserts specimen is undefined}
*/
@@ -141,7 +159,7 @@
*
* Assert an expected typeof result.
* @param {any} specimen The value to get the typeof
- * @param {Details=} optDetails The details to throw
+ * @param {Details} [optDetails] The details to throw
* @returns {asserts specimen is string}
*/
@@ -202,7 +220,7 @@
*
* @callback AssertQuote
* @param {any} payload What to declassify
- * @param {(string|number)=} spaces
+ * @param {(string|number)} [spaces]
* @returns {StringablePayload} The declassified payload
*/
@@ -235,8 +253,8 @@
* `optRaise` returns normally, which would be unusual, the throw following
* `optRaise(reason)` would still happen.
*
- * @param {Raise=} optRaise
- * @param {boolean=} unredacted
+ * @param {Raise} [optRaise]
+ * @param {boolean} [unredacted]
* @returns {Assert}
*/
@@ -400,6 +418,6 @@
* @callback FilterConsole
* @param {VirtualConsole} baseConsole
* @param {ConsoleFilter} filter
- * @param {string=} topic
+ * @param {string} [topic]
* @returns {VirtualConsole}
*/
diff --git a/packages/ses/src/permits.js b/packages/ses/src/permits.js
index d9a0df0046..bf3d33f4d2 100644
--- a/packages/ses/src/permits.js
+++ b/packages/ses/src/permits.js
@@ -82,6 +82,8 @@ export const universalPropertyNames = {
Iterator: 'Iterator',
// https://github.com/tc39/proposal-async-iterator-helpers
AsyncIterator: 'AsyncIterator',
+ // https://github.com/endojs/endo/issues/550
+ AggregateError: 'AggregateError',
// *** Other Properties of the Global Object
@@ -185,7 +187,6 @@ export const uniqueGlobalPropertyNames = {
// All the "subclasses" of Error. These are collectively represented in the
// ECMAScript spec by the meta variable NativeError.
-// TODO Add AggregateError https://github.com/Agoric/SES-shim/issues/550
export const NativeErrors = [
EvalError,
RangeError,
@@ -193,6 +194,8 @@ export const NativeErrors = [
SyntaxError,
TypeError,
URIError,
+ // https://github.com/endojs/endo/issues/550
+ AggregateError,
];
/**
@@ -599,6 +602,8 @@ export const permitted = {
SyntaxError: NativeError('%SyntaxErrorPrototype%'),
TypeError: NativeError('%TypeErrorPrototype%'),
URIError: NativeError('%URIErrorPrototype%'),
+ // https://github.com/endojs/endo/issues/550
+ AggregateError: NativeError('%AggregateErrorPrototype%'),
'%EvalErrorPrototype%': NativeErrorPrototype('EvalError'),
'%RangeErrorPrototype%': NativeErrorPrototype('RangeError'),
@@ -606,6 +611,8 @@ export const permitted = {
'%SyntaxErrorPrototype%': NativeErrorPrototype('SyntaxError'),
'%TypeErrorPrototype%': NativeErrorPrototype('TypeError'),
'%URIErrorPrototype%': NativeErrorPrototype('URIError'),
+ // https://github.com/endojs/endo/issues/550
+ '%AggregateErrorPrototype%': NativeErrorPrototype('AggregateError'),
// *** Numbers and Dates
@@ -1473,9 +1480,8 @@ export const permitted = {
'[[Proto]]': '%FunctionPrototype%',
all: fn,
allSettled: fn,
- // To transition from `false` to `fn` once we also have `AggregateError`
- // TODO https://github.com/Agoric/SES-shim/issues/550
- any: false, // ES2021
+ // https://github.com/Agoric/SES-shim/issues/550
+ any: fn,
prototype: '%PromisePrototype%',
race: fn,
reject: fn,
diff --git a/packages/ses/test/error/test-aggregate-error-console-demo.js b/packages/ses/test/error/test-aggregate-error-console-demo.js
new file mode 100644
index 0000000000..8472d7ce91
--- /dev/null
+++ b/packages/ses/test/error/test-aggregate-error-console-demo.js
@@ -0,0 +1,20 @@
+import test from 'ava';
+import '../../index.js';
+
+// This is the demo version of test-aggregate-error-console.js that
+// just outputs to the actual console, rather than using the logging console
+// to test. Its purpose is to eyeball rather than automated testing.
+// It also serves as a demo form of test-error-cause-console.js, since
+// it also shows console output for those cases.
+
+lockdown();
+
+test('aggregate error console demo', t => {
+ const e3 = Error('e3');
+ const e2 = Error('e2', { cause: e3 });
+ const u4 = URIError('u4', { cause: e2 });
+
+ const a1 = AggregateError([e3, u4], 'a1', { cause: e2 });
+ console.log('log1', a1);
+ t.is(a1.cause, e2);
+});
diff --git a/packages/ses/test/error/test-aggregate-error-console.js b/packages/ses/test/error/test-aggregate-error-console.js
new file mode 100644
index 0000000000..417fcc85c8
--- /dev/null
+++ b/packages/ses/test/error/test-aggregate-error-console.js
@@ -0,0 +1,44 @@
+import test from 'ava';
+import '../../index.js';
+import { throwsAndLogs } from './throws-and-logs.js';
+
+lockdown();
+
+test('aggregate error console', t => {
+ const e3 = Error('e3');
+ const e2 = Error('e2', { cause: e3 });
+ const u4 = URIError('u4', { cause: e2 });
+
+ const a1 = AggregateError([e3, u4], 'a1', { cause: e2 });
+ throwsAndLogs(
+ t,
+ () => {
+ console.log('log1', a1);
+ throw a1;
+ },
+ /a1/,
+ [
+ ['log', 'log1', '(AggregateError#1)'],
+ ['log', 'AggregateError#1:', 'a1'],
+ ['log', 'stack of AggregateError\n'],
+ ['log', 'AggregateError#1 cause:', '(Error#2)'],
+ ['log', 'AggregateError#1 errors:', '(Error#3)', '(URIError#4)'],
+ ['group', 'Nested 3 errors under AggregateError#1'],
+ ['log', 'Error#2:', 'e2'],
+ ['log', 'stack of Error\n'],
+ ['log', 'Error#2 cause:', '(Error#3)'],
+ ['group', 'Nested error under Error#2'],
+ ['log', 'Error#3:', 'e3'],
+ ['log', 'stack of Error\n'],
+ ['groupEnd'],
+ ['log', 'URIError#4:', 'u4'],
+ ['log', 'stack of URIError\n'],
+ ['log', 'URIError#4 cause:', '(Error#2)'],
+ ['group', 'Nested error under URIError#4'],
+ ['groupEnd'],
+ ['groupEnd'],
+ ['log', 'Caught', '(AggregateError#1)'],
+ ],
+ { wrapWithCausal: true },
+ );
+});
diff --git a/packages/ses/test/error/test-aggregate-error.js b/packages/ses/test/error/test-aggregate-error.js
new file mode 100644
index 0000000000..a95fbd114c
--- /dev/null
+++ b/packages/ses/test/error/test-aggregate-error.js
@@ -0,0 +1,46 @@
+import test from 'ava';
+import '../../index.js';
+
+const { getOwnPropertyDescriptor } = Object;
+
+lockdown();
+
+test('aggregate error', t => {
+ const e1 = Error('e1');
+ const e2 = Error('e2', { cause: e1 });
+ const u3 = URIError('u3', { cause: e1 });
+
+ const a4 = AggregateError([e2, u3], 'a4', { cause: e1 });
+ t.is(a4.message, 'a4');
+ t.is(a4.cause, e1);
+ t.deepEqual(getOwnPropertyDescriptor(a4, 'cause'), {
+ value: e1,
+ writable: true,
+ enumerable: false,
+ configurable: true,
+ });
+ t.deepEqual(getOwnPropertyDescriptor(a4, 'errors'), {
+ value: [e2, u3],
+ writable: true,
+ enumerable: false,
+ configurable: true,
+ });
+});
+
+test('Promise.any aggregate error', async t => {
+ const e1 = Error('e1');
+ const e2 = Error('e2', { cause: e1 });
+ const u3 = URIError('u3', { cause: e1 });
+
+ try {
+ await Promise.any([Promise.reject(e2), Promise.reject(u3)]);
+ } catch (a4) {
+ t.false('cause' in a4);
+ t.deepEqual(getOwnPropertyDescriptor(a4, 'errors'), {
+ value: [e2, u3],
+ writable: true,
+ enumerable: false,
+ configurable: true,
+ });
+ }
+});
diff --git a/packages/ses/test/error/test-error-cause-console.js b/packages/ses/test/error/test-error-cause-console.js
new file mode 100644
index 0000000000..323558d229
--- /dev/null
+++ b/packages/ses/test/error/test-error-cause-console.js
@@ -0,0 +1,80 @@
+import test from 'ava';
+import '../../index.js';
+import { throwsAndLogs } from './throws-and-logs.js';
+
+lockdown();
+
+test('error cause console control', t => {
+ const e1 = Error('e1');
+ throwsAndLogs(
+ t,
+ () => {
+ console.log('log1', e1);
+ throw e1;
+ },
+ /e1/,
+ [
+ ['log', 'log1', '(Error#1)'],
+ ['log', 'Error#1:', 'e1'],
+ ['log', 'stack of Error\n'],
+ ['log', 'Caught', '(Error#1)'],
+ ],
+ { wrapWithCausal: true },
+ );
+});
+
+test('error cause console one level', t => {
+ const e2 = Error('e2');
+ const e1 = Error('e1', { cause: e2 });
+ throwsAndLogs(
+ t,
+ () => {
+ console.log('log1', e1);
+ throw e1;
+ },
+ /e1/,
+ [
+ ['log', 'log1', '(Error#1)'],
+ ['log', 'Error#1:', 'e1'],
+ ['log', 'stack of Error\n'],
+ ['log', 'Error#1 cause:', '(Error#2)'],
+ ['group', 'Nested error under Error#1'],
+ ['log', 'Error#2:', 'e2'],
+ ['log', 'stack of Error\n'],
+ ['groupEnd'],
+ ['log', 'Caught', '(Error#1)'],
+ ],
+ { wrapWithCausal: true },
+ );
+});
+
+test('error cause console nested', t => {
+ const e3 = Error('e3');
+ const e2 = Error('e2', { cause: e3 });
+ const u1 = URIError('u1', { cause: e2 });
+ throwsAndLogs(
+ t,
+ () => {
+ console.log('log1', u1);
+ throw u1;
+ },
+ /u1/,
+ [
+ ['log', 'log1', '(URIError#1)'],
+ ['log', 'URIError#1:', 'u1'],
+ ['log', 'stack of URIError\n'],
+ ['log', 'URIError#1 cause:', '(Error#2)'],
+ ['group', 'Nested error under URIError#1'],
+ ['log', 'Error#2:', 'e2'],
+ ['log', 'stack of Error\n'],
+ ['log', 'Error#2 cause:', '(Error#3)'],
+ ['group', 'Nested error under Error#2'],
+ ['log', 'Error#3:', 'e3'],
+ ['log', 'stack of Error\n'],
+ ['groupEnd'],
+ ['groupEnd'],
+ ['log', 'Caught', '(URIError#1)'],
+ ],
+ { wrapWithCausal: true },
+ );
+});
diff --git a/packages/ses/test/error/test-error-cause.js b/packages/ses/test/error/test-error-cause.js
new file mode 100644
index 0000000000..0416285afd
--- /dev/null
+++ b/packages/ses/test/error/test-error-cause.js
@@ -0,0 +1,39 @@
+import test from 'ava';
+import '../../index.js';
+
+const { getOwnPropertyDescriptor } = Object;
+
+lockdown();
+
+test('error cause', t => {
+ const e1 = Error('e1');
+ t.is(e1.message, 'e1');
+ t.false('cause' in e1);
+ const e2 = Error('e2', { cause: e1 });
+ t.is(e2.message, 'e2');
+ t.is(e2.cause, e1);
+ t.deepEqual(getOwnPropertyDescriptor(e2, 'cause'), {
+ value: e1,
+ writable: true,
+ enumerable: false,
+ configurable: true,
+ });
+ const u3 = URIError('u3', { cause: e1 });
+ t.is(u3.message, 'u3');
+ t.is(u3.cause, e1);
+ t.deepEqual(getOwnPropertyDescriptor(u3, 'cause'), {
+ value: e1,
+ writable: true,
+ enumerable: false,
+ configurable: true,
+ });
+ const a4 = AggregateError([e2, u3], 'a4', { cause: e1 });
+ t.is(a4.message, 'a4');
+ t.is(a4.cause, e1);
+ t.deepEqual(getOwnPropertyDescriptor(a4, 'cause'), {
+ value: e1,
+ writable: true,
+ enumerable: false,
+ configurable: true,
+ });
+});
diff --git a/packages/ses/test/test-get-global-intrinsics.js b/packages/ses/test/test-get-global-intrinsics.js
index b17012e4b6..19b0531612 100644
--- a/packages/ses/test/test-get-global-intrinsics.js
+++ b/packages/ses/test/test-get-global-intrinsics.js
@@ -60,6 +60,8 @@ test.skip('getGlobalIntrinsics', () => {
'URIError',
'WeakMap',
'WeakSet',
+ // https://github.com/endojs/endo/issues/550
+ 'AggregateError',
// *** 18.4 Other Properties of the Global Object
diff --git a/packages/ses/types.d.ts b/packages/ses/types.d.ts
index 79fec88bcf..c198edd1ac 100644
--- a/packages/ses/types.d.ts
+++ b/packages/ses/types.d.ts
@@ -119,6 +119,8 @@ export type Details = string | DetailsToken;
export interface AssertMakeErrorOptions {
errorName?: string;
+ cause?: Error;
+ errors?: Error[];
}
type AssertTypeofBigint = (
@@ -175,6 +177,10 @@ interface ToStringable {
toString(): string;
}
+export type GenericErrorConstructor =
+ | ErrorConstructor
+ | AggregateErrorConstructor;
+
export type Raise = (reason: Error) => void;
// Behold: recursion.
// eslint-disable-next-line no-use-before-define
@@ -184,23 +190,29 @@ export interface AssertionFunctions {
(
value: any,
details?: Details,
- errorConstructor?: ErrorConstructor,
+ errConstructor?: GenericErrorConstructor,
+ options?: AssertMakeErrorOptions,
): asserts value;
typeof: AssertTypeof;
equal(
left: any,
right: any,
details?: Details,
- errorConstructor?: ErrorConstructor,
+ errConstructor?: GenericErrorConstructor,
+ options?: AssertMakeErrorOptions,
): void;
string(specimen: any, details?: Details): asserts specimen is string;
- fail(details?: Details, errorConstructor?: ErrorConstructor): never;
+ fail(
+ details?: Details,
+ errConstructor?: GenericErrorConstructor,
+ options?: AssertMakeErrorOptions,
+ ): never;
}
export interface AssertionUtilities {
error(
details?: Details,
- errorConstructor?: ErrorConstructor,
+ errConstructor?: GenericErrorConstructor,
options?: AssertMakeErrorOptions,
): Error;
note(error: Error, details: Details): void;