Commit 37d4e56a authored by Adam Rice's avatar Adam Rice Committed by Commit Bot

Transferable Streams: Remove exception special handling

Serialising and deserialising exceptions is now supported directly, so
transferable streams do not have to work around the lack of support.
Remove the workarounds.

Change the semantics so that failures to serialise are not hidden by
implicit conversion to undefined. Instead the cancel() or abort() method
will return a rejection, and the stream will be errored.

Update the http/streams/transferable/reason.html web test to reflect the
new semantics.

BUG=894838

Change-Id: I44c5bd968b4544df2706269900ca8461eabfa00c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2257655Reviewed-by: default avatarYutaka Hirano <yhirano@chromium.org>
Commit-Queue: Adam Rice <ricea@chromium.org>
Cr-Commit-Position: refs/heads/master@{#782976}
parent 121c131b
...@@ -52,22 +52,6 @@ namespace { ...@@ -52,22 +52,6 @@ namespace {
// These are the types of messages that are sent between peers. // These are the types of messages that are sent between peers.
enum class MessageType { kPull, kCancel, kChunk, kClose, kAbort, kError }; enum class MessageType { kPull, kCancel, kChunk, kClose, kAbort, kError };
// These are the different ways an error reason can be encoded.
enum class ErrorType { kTypeError, kJson, kDomException, kUndefined };
bool IsATypeError(ScriptState* script_state, v8::Local<v8::Object> object) {
// There isn't a 100% reliable way to identify a TypeError.
return object->IsNativeError() &&
object->GetConstructorName()
->Equals(script_state->GetContext(),
V8AtomicString(script_state->GetIsolate(), "TypeError"))
.ToChecked();
}
bool IsADOMException(v8::Isolate* isolate, v8::Local<v8::Object> object) {
return V8DOMException::HasInstance(object, isolate);
}
// Creates a JavaScript object with a null prototype structured like {key1: // Creates a JavaScript object with a null prototype structured like {key1:
// value2, key2: value2}. This is used to create objects to be serialized by // value2, key2: value2}. This is used to create objects to be serialized by
// postMessage. // postMessage.
...@@ -107,9 +91,8 @@ bool UnpackKeyValueObject(ScriptState* script_state, ...@@ -107,9 +91,8 @@ bool UnpackKeyValueObject(ScriptState* script_state,
return true; return true;
} }
// Send a message with type |type| and contents |value| over |port|. The type // Sends a message with type |type| and contents |value| over |port|. The type
// will be packed as a number with key "t", and the value will be packed with // is packed as a number with key "t", and the value is packed with key "v".
// key "v".
void PackAndPostMessage(ScriptState* script_state, void PackAndPostMessage(ScriptState* script_state,
MessagePort* port, MessagePort* port,
MessageType type, MessageType type,
...@@ -125,198 +108,41 @@ void PackAndPostMessage(ScriptState* script_state, ...@@ -125,198 +108,41 @@ void PackAndPostMessage(ScriptState* script_state,
PostMessageOptions::Create(), exception_state); PostMessageOptions::Create(), exception_state);
} }
// Packs an error into an {e: number, s: string} object for transmission by // Sends a kError message to the remote side, disregarding failure.
// postMessage. Serializing the resulting object should never fail. void SendError(ScriptState* script_state,
v8::Local<v8::Object> PackErrorType(v8::Isolate* isolate, MessagePort* port,
ErrorType type, v8::Local<v8::Value> error) {
v8::Local<v8::String> string) { ExceptionState exception_state(script_state->GetIsolate(),
auto error_as_number = v8::Number::New(isolate, static_cast<int>(type)); ExceptionState::kUnknownContext, "", "");
return CreateKeyValueObject(isolate, "e", error_as_number, "s", string); PackAndPostMessage(script_state, port, MessageType::kError, error,
} exception_state);
if (exception_state.HadException()) {
// Overload for the common case where |string| is a compile-time constant. DLOG(WARNING) << "Disregarding exception while sending error";
v8::Local<v8::Object> PackErrorType(v8::Isolate* isolate, exception_state.ClearException();
ErrorType type,
const char* string) {
return PackErrorType(isolate, type, V8String(isolate, string));
}
// We'd like to able to transfer TypeError exceptions, but we can't, so we hack
// around it. PackReason() is guaranteed to succeed and the object produced is
// guaranteed to be serializable by postMessage(), however data may be lost. It
// is not very efficient, and has fairly arbitrary semantics.
// TODO(ricea): Replace once Errors are serializable.
v8::Local<v8::Value> PackReason(ScriptState* script_state,
v8::Local<v8::Value> reason) {
auto* isolate = script_state->GetIsolate();
auto context = script_state->GetContext();
if (reason->IsString() || reason->IsNumber() || reason->IsBoolean()) {
v8::TryCatch try_catch(isolate);
v8::Local<v8::String> stringified;
if (!v8::JSON::Stringify(context, reason).ToLocal(&stringified)) {
return PackErrorType(isolate, ErrorType::kTypeError,
"Cannot transfer message");
}
return PackErrorType(isolate, ErrorType::kJson, stringified);
}
if (reason->IsNull()) {
return PackErrorType(isolate, ErrorType::kJson, "null");
}
if (reason->IsFunction() || reason->IsSymbol() || !reason->IsObject()) {
// Squash to undefined
return PackErrorType(isolate, ErrorType::kUndefined, "");
}
if (IsATypeError(script_state, reason.As<v8::Object>())) {
v8::TryCatch try_catch(isolate);
// "message" on TypeError is a normal property, meaning that if it
// is set, it is set on the object itself. We can take advantage of
// this to avoid executing user JavaScript in the case when the
// TypeError was generated internally.
v8::Local<v8::Value> descriptor;
if (!reason.As<v8::Object>()
->GetOwnPropertyDescriptor(context,
V8AtomicString(isolate, "message"))
.ToLocal(&descriptor)) {
return PackErrorType(isolate, ErrorType::kTypeError,
"Cannot transfer message");
}
if (descriptor->IsUndefined()) {
return PackErrorType(isolate, ErrorType::kTypeError, "");
}
v8::Local<v8::Value> message;
CHECK(descriptor->IsObject());
if (!descriptor.As<v8::Object>()
->Get(context, V8AtomicString(isolate, "value"))
.ToLocal(&message)) {
message = V8String(isolate, "Cannot transfer message");
} else if (!message->IsString()) {
message = V8String(isolate, "");
}
return PackErrorType(isolate, ErrorType::kTypeError,
message.As<v8::String>());
}
if (IsADOMException(isolate, reason.As<v8::Object>())) {
DOMException* dom_exception =
V8DOMException::ToImpl(reason.As<v8::Object>());
String message = dom_exception->message();
String name = dom_exception->name();
v8::Local<v8::Value> packed = CreateKeyValueObject(
isolate, "m", V8String(isolate, message), "n", V8String(isolate, name));
// It should be impossible for this to fail, except for out-of-memory.
v8::Local<v8::String> packed_string =
v8::JSON::Stringify(context, packed).ToLocalChecked();
return PackErrorType(isolate, ErrorType::kDomException, packed_string);
}
v8::TryCatch try_catch(isolate);
v8::Local<v8::Value> json;
if (!v8::JSON::Stringify(context, reason).ToLocal(&json)) {
return PackErrorType(isolate, ErrorType::kTypeError,
"Cannot transfer message");
} }
return PackErrorType(isolate, ErrorType::kJson, json.As<v8::String>());
} }
// Converts an object created by PackReason() back into a clone of the original // Same as PackAndPostMessage(), except that it attempts to handle exceptions by
// object, minus any data that was discarded by PackReason(). // sending a kError message to the remote side. On failure |error| is set to the
bool UnpackReason(ScriptState* script_state, // original exception and the function returns false. Any error from sending the
v8::Local<v8::Value> packed_reason, // kError message is ignored.
v8::Local<v8::Value>* reason) { bool PackAndPostMessageHandlingExceptions(ScriptState* script_state,
// We need to be robust against malformed input because it could come from a MessagePort* port,
// compromised renderer. MessageType type,
if (!packed_reason->IsObject()) { v8::Local<v8::Value> value,
DLOG(WARNING) << "packed_reason is not an object"; v8::Local<v8::Value>* error) {
return false; ExceptionState exception_state(script_state->GetIsolate(),
} ExceptionState::kUnknownContext, "", "");
PackAndPostMessage(script_state, port, type, value, exception_state);
v8::Local<v8::Value> encoder_value;
v8::Local<v8::Value> string_value;
if (!UnpackKeyValueObject(script_state, packed_reason.As<v8::Object>(), "e",
&encoder_value, "s", &string_value)) {
return false;
}
if (!encoder_value->IsNumber()) {
DLOG(WARNING) << "encoder_value is not a number";
return false;
}
int encoder = encoder_value.As<v8::Number>()->Value(); if (exception_state.HadException()) {
if (!string_value->IsString()) { *error = exception_state.GetException();
DLOG(WARNING) << "string_value is not a string"; SendError(script_state, port, *error);
exception_state.ClearException();
return false; return false;
} }
v8::Local<v8::String> string = string_value.As<v8::String>(); return true;
auto* isolate = script_state->GetIsolate();
auto context = script_state->GetContext();
switch (static_cast<ErrorType>(encoder)) {
case ErrorType::kJson: {
v8::TryCatch try_catch(isolate);
if (!v8::JSON::Parse(context, string).ToLocal(reason)) {
DLOG(WARNING) << "JSON Parse failed. Content: " << ToCoreString(string);
return false;
}
return true;
}
case ErrorType::kTypeError:
*reason = v8::Exception::TypeError(string);
return true;
case ErrorType::kDomException: {
v8::TryCatch try_catch(isolate);
v8::Local<v8::Value> packed_exception;
if (!v8::JSON::Parse(context, string).ToLocal(&packed_exception)) {
DLOG(WARNING) << "Packed DOMException JSON parse failed";
return false;
}
if (!packed_exception->IsObject()) {
DLOG(WARNING) << "Packed DOMException is not an object";
return false;
}
v8::Local<v8::Value> message;
v8::Local<v8::Value> name;
if (!UnpackKeyValueObject(script_state, packed_exception.As<v8::Object>(),
"m", &message, "n", &name)) {
DLOG(WARNING) << "Failed unpacking packed DOMException";
return false;
}
if (!message->IsString()) {
DLOG(WARNING) << "DOMException message is not a string";
return false;
}
if (!name->IsString()) {
DLOG(WARNING) << "DOMException name is not a string";
return false;
}
auto ToBlink = [](v8::Local<v8::Value> value) {
return ToBlinkString<String>(value.As<v8::String>(), kDoNotExternalize);
};
*reason = ToV8(DOMException::Create(ToBlink(message), ToBlink(name)),
script_state);
return true;
}
case ErrorType::kUndefined:
*reason = v8::Undefined(isolate);
return true;
default:
DLOG(WARNING) << "Invalid ErrorType: " << encoder;
return false;
}
} }
// Base class for CrossRealmTransformWritable and CrossRealmTransformReadable. // Base class for CrossRealmTransformWritable and CrossRealmTransformReadable.
...@@ -398,15 +224,8 @@ class CrossRealmTransformErrorListener final : public NativeEventListener { ...@@ -398,15 +224,8 @@ class CrossRealmTransformErrorListener final : public NativeEventListener {
DOMException::Create("chunk could not be cloned", "DataCloneError"); DOMException::Create("chunk could not be cloned", "DataCloneError");
auto* message_port = target_->GetMessagePort(); auto* message_port = target_->GetMessagePort();
v8::Local<v8::Value> error_value = ToV8(error, script_state); v8::Local<v8::Value> error_value = ToV8(error, script_state);
ExceptionState exception_state(script_state->GetIsolate(),
ExceptionState::kUnknownContext, "", ""); SendError(script_state, message_port, error_value);
PackAndPostMessage(script_state, message_port, MessageType::kError,
PackReason(script_state, error_value), exception_state);
if (exception_state.HadException()) {
DLOG(WARNING) << "Ignoring postMessage failure in error listener";
exception_state.ClearException();
}
message_port->close(); message_port->close();
target_->HandleError(error_value); target_->HandleError(error_value);
...@@ -520,24 +339,14 @@ class CrossRealmTransformWritable::WriteAlgorithm final ...@@ -520,24 +339,14 @@ class CrossRealmTransformWritable::WriteAlgorithm final
v8::Local<v8::Value> chunk) { v8::Local<v8::Value> chunk) {
writable_->backpressure_promise_ = writable_->backpressure_promise_ =
MakeGarbageCollected<StreamPromiseResolver>(script_state); MakeGarbageCollected<StreamPromiseResolver>(script_state);
ExceptionState exception_state(script_state->GetIsolate(),
ExceptionState::kUnknownContext, "", "");
PackAndPostMessage(script_state, writable_->message_port_,
MessageType::kChunk, chunk, exception_state);
if (exception_state.HadException()) {
auto exception = exception_state.GetException();
exception_state.ClearException();
PackAndPostMessage(
script_state, writable_->message_port_, MessageType::kError,
PackReason(writable_->script_state_, exception), exception_state);
if (exception_state.HadException()) {
DLOG(WARNING) << "Disregarding exception while sending error";
exception_state.ClearException();
}
v8::Local<v8::Value> error;
bool success = PackAndPostMessageHandlingExceptions(
script_state, writable_->message_port_, MessageType::kChunk, chunk,
&error);
if (!success) {
writable_->message_port_->close(); writable_->message_port_->close();
return PromiseReject(script_state, exception); return PromiseReject(script_state, error);
} }
return PromiseResolveWithUndefined(script_state); return PromiseResolveWithUndefined(script_state);
...@@ -557,17 +366,18 @@ class CrossRealmTransformWritable::CloseAlgorithm final ...@@ -557,17 +366,18 @@ class CrossRealmTransformWritable::CloseAlgorithm final
int argc, int argc,
v8::Local<v8::Value> argv[]) override { v8::Local<v8::Value> argv[]) override {
DCHECK_EQ(argc, 0); DCHECK_EQ(argc, 0);
ExceptionState exception_state(script_state->GetIsolate(),
ExceptionState::kUnknownContext, "", ""); v8::Local<v8::Value> error;
PackAndPostMessage( bool success = PackAndPostMessageHandlingExceptions(
script_state, writable_->message_port_, MessageType::kClose, script_state, writable_->message_port_, MessageType::kClose,
v8::Undefined(script_state->GetIsolate()), exception_state); v8::Undefined(script_state->GetIsolate()), &error);
if (exception_state.HadException()) {
DLOG(WARNING) << "Ignoring exception from PackAndPostMessage kClose";
exception_state.ClearException();
}
writable_->message_port_->close(); writable_->message_port_->close();
if (!success) {
return PromiseReject(script_state, error);
}
return PromiseResolveWithUndefined(script_state); return PromiseResolveWithUndefined(script_state);
} }
...@@ -592,16 +402,18 @@ class CrossRealmTransformWritable::AbortAlgorithm final ...@@ -592,16 +402,18 @@ class CrossRealmTransformWritable::AbortAlgorithm final
v8::Local<v8::Value> argv[]) override { v8::Local<v8::Value> argv[]) override {
DCHECK_EQ(argc, 1); DCHECK_EQ(argc, 1);
auto reason = argv[0]; auto reason = argv[0];
ExceptionState exception_state(script_state->GetIsolate(),
ExceptionState::kUnknownContext, "", ""); v8::Local<v8::Value> error;
PackAndPostMessage( bool success = PackAndPostMessageHandlingExceptions(
script_state, writable_->message_port_, MessageType::kAbort, script_state, writable_->message_port_, MessageType::kAbort, reason,
PackReason(writable_->script_state_, reason), exception_state); &error);
if (exception_state.HadException()) {
DLOG(WARNING) << "Ignoring exception from PackAndPostMessage kAbort";
exception_state.ClearException();
}
writable_->message_port_->close(); writable_->message_port_->close();
if (!success) {
return PromiseReject(script_state, error);
}
return PromiseResolveWithUndefined(script_state); return PromiseResolveWithUndefined(script_state);
} }
...@@ -649,14 +461,8 @@ void CrossRealmTransformWritable::HandleMessage(MessageType type, ...@@ -649,14 +461,8 @@ void CrossRealmTransformWritable::HandleMessage(MessageType type,
case MessageType::kCancel: case MessageType::kCancel:
case MessageType::kError: { case MessageType::kError: {
v8::Local<v8::Value> reason;
if (!UnpackReason(script_state_, value, &reason)) {
DLOG(WARNING)
<< "Invalid message from peer ignored (unable to unpack value)";
return;
}
WritableStreamDefaultController::ErrorIfNeeded(script_state_, controller_, WritableStreamDefaultController::ErrorIfNeeded(script_state_, controller_,
reason); value);
if (backpressure_promise_) { if (backpressure_promise_) {
backpressure_promise_->ResolveWithUndefined(script_state_); backpressure_promise_->ResolveWithUndefined(script_state_);
backpressure_promise_ = nullptr; backpressure_promise_ = nullptr;
...@@ -725,15 +531,15 @@ class CrossRealmTransformReadable::PullAlgorithm final ...@@ -725,15 +531,15 @@ class CrossRealmTransformReadable::PullAlgorithm final
v8::Local<v8::Value> argv[]) override { v8::Local<v8::Value> argv[]) override {
DCHECK_EQ(argc, 0); DCHECK_EQ(argc, 0);
auto* isolate = script_state->GetIsolate(); auto* isolate = script_state->GetIsolate();
ExceptionState exception_state(isolate, ExceptionState::kUnknownContext, "",
"");
PackAndPostMessage( v8::Local<v8::Value> error;
bool success = PackAndPostMessageHandlingExceptions(
script_state, readable_->message_port_, MessageType::kPull, script_state, readable_->message_port_, MessageType::kPull,
v8::Undefined(script_state->GetIsolate()), exception_state); v8::Undefined(isolate), &error);
if (exception_state.HadException()) {
DLOG(WARNING) << "Ignoring exception from PackAndPostMessage kClose"; if (!success) {
exception_state.ClearException(); readable_->message_port_->close();
return PromiseReject(script_state, error);
} }
return readable_->backpressure_promise_->V8Promise(isolate); return readable_->backpressure_promise_->V8Promise(isolate);
...@@ -761,18 +567,18 @@ class CrossRealmTransformReadable::CancelAlgorithm final ...@@ -761,18 +567,18 @@ class CrossRealmTransformReadable::CancelAlgorithm final
DCHECK_EQ(argc, 1); DCHECK_EQ(argc, 1);
auto reason = argv[0]; auto reason = argv[0];
readable_->finished_ = true; readable_->finished_ = true;
ExceptionState exception_state(script_state->GetIsolate(),
ExceptionState::kUnknownContext, "", ""); v8::Local<v8::Value> error;
bool success = PackAndPostMessageHandlingExceptions(
PackAndPostMessage(script_state, readable_->message_port_, script_state, readable_->message_port_, MessageType::kCancel, reason,
MessageType::kCancel, PackReason(script_state, reason), &error);
exception_state);
if (exception_state.HadException()) {
DLOG(WARNING) << "Ignoring exception from PackAndPostMessage kClose";
exception_state.ClearException();
}
readable_->message_port_->close(); readable_->message_port_->close();
if (!success) {
return PromiseReject(script_state, error);
}
return PromiseResolveWithUndefined(script_state); return PromiseResolveWithUndefined(script_state);
} }
...@@ -837,15 +643,7 @@ void CrossRealmTransformReadable::HandleMessage(MessageType type, ...@@ -837,15 +643,7 @@ void CrossRealmTransformReadable::HandleMessage(MessageType type,
case MessageType::kAbort: case MessageType::kAbort:
case MessageType::kError: { case MessageType::kError: {
finished_ = true; finished_ = true;
v8::Local<v8::Value> reason; ReadableStreamDefaultController::Error(script_state_, controller_, value);
if (!UnpackReason(script_state_, value, &reason)) {
DLOG(WARNING)
<< "Invalid message from peer ignored (unable to unpack value)";
return;
}
ReadableStreamDefaultController::Error(script_state_, controller_,
reason);
message_port_->close(); message_port_->close();
return; return;
} }
......
...@@ -6,23 +6,26 @@ ...@@ -6,23 +6,26 @@
<script> <script>
'use strict'; 'use strict';
// These tests verify the algorithm for passing through error reasons from one // Chrome used to special-case the reason for cancel() and abort() in order to
// realm to another. We only test these for cancel on a ReadableStream, and // handle exceptions correctly. This is no longer necessary. These tests are
// assume that all the other places that should be using the same algorithm are // retained to avoid regressions.
// doing so.
function getTransferredReason(originalReason) { async function getTransferredReason(originalReason) {
return new Promise((resolve, reject) => { let resolvePromise;
createTransferredReadableStream({ const rv = new Promise(resolve => {
cancel(reason) { resolvePromise = resolve;
resolve(reason);
}
}).then(rs => rs.cancel(originalReason))
.catch(reject);
}); });
const rs = await createTransferredReadableStream({
cancel(reason) {
resolvePromise(reason);
}
});
await rs.cancel(originalReason);
return rv;
} }
for (const value of ['hi', '\t\r\n', 7, 3.0, undefined, null, true, false]) { for (const value of ['hi', '\t\r\n', 7, 3.0, undefined, null, true, false,
NaN, Infinity]) {
promise_test(async () => { promise_test(async () => {
const reason = await getTransferredReason(value); const reason = await getTransferredReason(value);
assert_equals(reason, value, 'reason should match'); assert_equals(reason, value, 'reason should match');
...@@ -30,17 +33,12 @@ for (const value of ['hi', '\t\r\n', 7, 3.0, undefined, null, true, false]) { ...@@ -30,17 +33,12 @@ for (const value of ['hi', '\t\r\n', 7, 3.0, undefined, null, true, false]) {
} }
for (const badType of [Symbol('hi'), _ => 'hi']) { for (const badType of [Symbol('hi'), _ => 'hi']) {
promise_test(async () => { promise_test(async t => {
const reason = await getTransferredReason(badType); return promise_rejects_dom(t, 'DataCloneError',
assert_equals(reason, undefined, 'reason should be undefined'); getTransferredReason(badType),
}, `reason with a type of '${typeof badType}' should be squished to undefined`); 'cancel() should reject');
} }, `reason with a type of '${typeof badType}' should be rejected and ` +
`error the stream`);
for (const badNumber of [NaN, Infinity]) {
promise_test(async () => {
const reason = await getTransferredReason(badNumber);
assert_equals(reason, null, 'reason should be null');
}, `number with a value of '${badNumber}' should be squished to null`);
} }
promise_test(async () => { promise_test(async () => {
...@@ -55,8 +53,10 @@ promise_test(async () => { ...@@ -55,8 +53,10 @@ promise_test(async () => {
const circularObject = {}; const circularObject = {};
circularObject.self = circularObject; circularObject.self = circularObject;
const reason = await getTransferredReason(circularObject); const reason = await getTransferredReason(circularObject);
assert_true(reason instanceof TypeError, 'a TypeError should be output'); assert_true(reason instanceof Object, 'an Object should be output');
}, 'objects that cannot be expressed in JSON should result in a TypeError'); assert_equals(reason.self, reason,
'the object should have a circular reference');
}, 'objects that cannot be expressed in JSON should also be preserved');
promise_test(async () => { promise_test(async () => {
const originalReason = new TypeError('hi'); const originalReason = new TypeError('hi');
...@@ -77,10 +77,10 @@ promise_test(async () => { ...@@ -77,10 +77,10 @@ promise_test(async () => {
promise_test(async () => { promise_test(async () => {
const originalReason = new TypeError(); const originalReason = new TypeError();
originalReason.message = {}; originalReason.message = [1, 2, 3];
const reason = await getTransferredReason(originalReason); const reason = await getTransferredReason(originalReason);
assert_equals(reason.message, '', 'message should not be preserved'); assert_equals(reason.message, '1,2,3', 'message should be stringified');
}, 'a TypeError message should not be preserved if it is not a string'); }, 'a TypeError message should be converted to a string');
promise_test(async () => { promise_test(async () => {
const originalReason = new TypeError(); const originalReason = new TypeError();
...@@ -111,17 +111,22 @@ promise_test(async () => { ...@@ -111,17 +111,22 @@ promise_test(async () => {
'the names should match'); 'the names should match');
}, 'DOMException errors should be preserved'); }, 'DOMException errors should be preserved');
promise_test(async () => { for (const errorConstructor of [EvalError, RangeError,
const originalReason = new RangeError('nope'); ReferenceError, SyntaxError, TypeError,
const reason = await getTransferredReason(originalReason); URIError]) {
assert_equals(typeof reason, 'object', 'reason should have type object'); promise_test(async () => {
assert_false(reason instanceof RangeError, const originalReason = new errorConstructor('nope');
'reason should not be a RangeError'); const reason = await getTransferredReason(originalReason);
assert_false(reason instanceof Error, assert_equals(typeof reason, 'object', 'reason should have type object');
'reason should not be an Error'); assert_true(reason instanceof errorConstructor,
assert_equals(reason.constructor, Object, 'reason should be an Object'); `reason should inherit ${errorConstructor.name}`);
assert_array_equals(Object.getOwnPropertyNames(reason), [], assert_true(reason instanceof Error, 'reason should inherit Error');
'reason should have no properties'); assert_equals(reason.constructor, errorConstructor,
}, 'RangeErrors should not be preserved'); 'reason should have the right constructor');
assert_equals(reason.name, errorConstructor.name,
`name should match constructor name`);
assert_equals(reason.message, 'nope', 'message should match');
}, `${errorConstructor.name} should be preserved`);
}
</script> </script>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment