From f1357866039d2c712a056035002aa9dee5d7d2e8 Mon Sep 17 00:00:00 2001
From: geek <wpreul@gmail.com>
Date: Sat, 9 Sep 2017 20:36:47 -0500
Subject: [PATCH] assert: support custom errors

---
 doc/api/assert.md                 | 59 +++++++++++++++++++++----------
 lib/assert.js                     |  2 ++
 test/parallel/test-assert-fail.js | 22 ++++++++++++
 test/parallel/test-assert.js      | 28 +++++++++++++++
 4 files changed, 93 insertions(+), 18 deletions(-)

diff --git a/doc/api/assert.md b/doc/api/assert.md
index 254d4faf98bfbf..50bc22e0b66714 100644
--- a/doc/api/assert.md
+++ b/doc/api/assert.md
@@ -101,7 +101,9 @@ assert.deepEqual(obj1, obj4);
 
 If the values are not equal, an `AssertionError` is thrown with a `message`
 property set equal to the value of the `message` parameter. If the `message`
-parameter is undefined, a default error message is assigned.
+parameter is undefined, a default error message is assigned. If the `message`
+parameter is an instance of an `Error` then it will be thrown instead of the
+`AssertionError`.
 
 ## assert.deepStrictEqual(actual, expected[, message])
 <!-- YAML
@@ -174,7 +176,9 @@ assert.deepStrictEqual(NaN, NaN);
 
 If the values are not equal, an `AssertionError` is thrown with a `message`
 property set equal to the value of the `message` parameter. If the `message`
-parameter is undefined, a default error message is assigned.
+parameter is undefined, a default error message is assigned. If the `message`
+parameter is an instance of an `Error` then it will be thrown instead of the
+`AssertionError`.
 
 ## assert.doesNotThrow(block[, error][, message])
 <!-- YAML
@@ -268,7 +272,9 @@ assert.equal({ a: { b: 1 } }, { a: { b: 1 } });
 
 If the values are not equal, an `AssertionError` is thrown with a `message`
 property set equal to the value of the `message` parameter. If the `message`
-parameter is undefined, a default error message is assigned.
+parameter is undefined, a default error message is assigned. If the `message`
+parameter is an instance of an `Error` then it will be thrown instead of the
+`AssertionError`.
 
 ## assert.fail([message])
 ## assert.fail(actual, expected[, message[, operator[, stackStartFunction]]])
@@ -282,13 +288,15 @@ added: v0.1.21
 * `stackStartFunction` {function} (default: `assert.fail`)
 
 Throws an `AssertionError`. If `message` is falsy, the error message is set as
-the values of `actual` and `expected` separated by the provided `operator`.
-If just the two `actual` and `expected` arguments are provided, `operator` will
-default to `'!='`. If `message` is provided only it will be used as the error
-message, the other arguments will be stored as properties on the thrown object.
-If `stackStartFunction` is provided, all stack frames above that function will
-be removed from stacktrace (see [`Error.captureStackTrace`]). If no arguments
-are given, the default message `Failed` will be used.
+the values of `actual` and `expected` separated by the provided `operator`. If
+the `message` parameter is an instance of an `Error` then it will be thrown
+instead of the `AssertionError`. If just the two `actual` and `expected`
+arguments are provided, `operator` will default to `'!='`. If `message` is
+provided only it will be used as the error message, the other arguments will be
+stored as properties on the thrown object. If `stackStartFunction` is provided,
+all stack frames above that function will be removed from stacktrace (see
+[`Error.captureStackTrace`]). If no arguments are given, the default message
+`Failed` will be used.
 
 ```js
 const assert = require('assert');
@@ -301,6 +309,9 @@ assert.fail(1, 2, 'fail');
 
 assert.fail(1, 2, 'whoops', '>');
 // AssertionError [ERR_ASSERTION]: whoops
+
+assert.fail(1, 2, new TypeError('need array'));
+// TypeError: need array
 ```
 
 *Note*: Is the last two cases `actual`, `expected`, and `operator` have no
@@ -412,7 +423,9 @@ assert.notDeepEqual(obj1, obj4);
 
 If the values are deeply equal, an `AssertionError` is thrown with a `message`
 property set equal to the value of the `message` parameter. If the `message`
-parameter is undefined, a default error message is assigned.
+parameter is undefined, a default error message is assigned. If the `message`
+parameter is an instance of an `Error` then it will be thrown instead of the
+`AssertionError`.
 
 ## assert.notDeepStrictEqual(actual, expected[, message])
 <!-- YAML
@@ -453,9 +466,11 @@ assert.notDeepStrictEqual({ a: 1 }, { a: '1' });
 // OK
 ```
 
-If the values are deeply and strictly equal, an `AssertionError` is thrown
-with a `message` property set equal to the value of the `message` parameter. If
-the `message` parameter is undefined, a default error message is assigned.
+If the values are deeply and strictly equal, an `AssertionError` is thrown with
+a `message` property set equal to the value of the `message` parameter. If the
+`message` parameter is undefined, a default error message is assigned. If the
+`message` parameter is an instance of an `Error` then it will be thrown instead
+of the `AssertionError`.
 
 ## assert.notEqual(actual, expected[, message])
 <!-- YAML
@@ -483,7 +498,9 @@ assert.notEqual(1, '1');
 
 If the values are equal, an `AssertionError` is thrown with a `message`
 property set equal to the value of the `message` parameter. If the `message`
-parameter is undefined, a default error message is assigned.
+parameter is undefined, a default error message is assigned. If the `message`
+parameter is an instance of an `Error` then it will be thrown instead of the
+`AssertionError`.
 
 ## assert.notStrictEqual(actual, expected[, message])
 <!-- YAML
@@ -511,7 +528,9 @@ assert.notStrictEqual(1, '1');
 
 If the values are strictly equal, an `AssertionError` is thrown with a
 `message` property set equal to the value of the `message` parameter. If the
-`message` parameter is undefined, a default error message is assigned.
+`message` parameter is undefined, a default error message is assigned. If the
+`message` parameter is an instance of an `Error` then it will be thrown instead
+of the `AssertionError`.
 
 ## assert.ok(value[, message])
 <!-- YAML
@@ -525,7 +544,9 @@ Tests if `value` is truthy. It is equivalent to
 
 If `value` is not truthy, an `AssertionError` is thrown with a `message`
 property set equal to the value of the `message` parameter. If the `message`
-parameter is `undefined`, a default error message is assigned.
+parameter is `undefined`, a default error message is assigned. If the `message`
+parameter is an instance of an `Error` then it will be thrown instead of the
+`AssertionError`.
 
 ```js
 const assert = require('assert');
@@ -568,7 +589,9 @@ assert.strictEqual(1, '1');
 
 If the values are not strictly equal, an `AssertionError` is thrown with a
 `message` property set equal to the value of the `message` parameter. If the
-`message` parameter is undefined, a default error message is assigned.
+`message` parameter is undefined, a default error message is assigned. If the
+`message` parameter is an instance of an `Error` then it will be thrown instead
+of the `AssertionError`.
 
 ## assert.throws(block[, error][, message])
 <!-- YAML
diff --git a/lib/assert.js b/lib/assert.js
index 2c52fcc19996f9..6cbebcc4d87ed5 100644
--- a/lib/assert.js
+++ b/lib/assert.js
@@ -38,6 +38,8 @@ const assert = module.exports = ok;
 // display purposes.
 
 function innerFail(actual, expected, message, operator, stackStartFunction) {
+  if (message instanceof Error) throw message;
+
   throw new errors.AssertionError({
     message,
     actual,
diff --git a/test/parallel/test-assert-fail.js b/test/parallel/test-assert-fail.js
index efa48cc215cfdb..14d28e5cd0045f 100644
--- a/test/parallel/test-assert-fail.js
+++ b/test/parallel/test-assert-fail.js
@@ -28,6 +28,17 @@ common.expectsError(() => {
   expected: undefined
 });
 
+// One arg = Error
+common.expectsError(() => {
+  assert.fail(new TypeError('custom message'));
+}, {
+  type: TypeError,
+  message: 'custom message',
+  operator: undefined,
+  actual: undefined,
+  expected: undefined
+});
+
 // Two args only, operator defaults to '!='
 common.expectsError(() => {
   assert.fail('first', 'second');
@@ -52,6 +63,17 @@ common.expectsError(() => {
   expected: 'ignored'
 });
 
+// Three args with custom Error
+common.expectsError(() => {
+  assert.fail(typeof 1, 'object', new TypeError('another custom message'));
+}, {
+  type: TypeError,
+  message: 'another custom message',
+  operator: undefined,
+  actual: 'number',
+  expected: 'object'
+});
+
 // No third arg (but a fourth arg)
 common.expectsError(() => {
   assert.fail('first', 'second', undefined, 'operator');
diff --git a/test/parallel/test-assert.js b/test/parallel/test-assert.js
index 30e80274890ce6..d4cd52c37c3839 100644
--- a/test/parallel/test-assert.js
+++ b/test/parallel/test-assert.js
@@ -645,6 +645,34 @@ try {
                      'Message incorrectly marked as generated');
 }
 
+{
+  let threw = false;
+  const rangeError = new RangeError('my range');
+
+  // verify custom errors
+  try {
+    assert.strictEqual(1, 2, rangeError);
+  } catch (e) {
+    assert.strictEqual(e, rangeError);
+    threw = true;
+    assert.ok(e instanceof RangeError, 'Incorrect error type thrown');
+  }
+  assert.ok(threw);
+  threw = false;
+
+  // verify AssertionError is the result from doesNotThrow with custom Error
+  try {
+    assert.doesNotThrow(() => {
+      throw new TypeError('wrong type');
+    }, TypeError, rangeError);
+  } catch (e) {
+    threw = true;
+    assert.ok(e.message.includes(rangeError.message));
+    assert.ok(e instanceof assert.AssertionError);
+  }
+  assert.ok(threw);
+}
+
 {
   // Verify that throws() and doesNotThrow() throw on non-function block
   function typeName(value) {