Repository: avajs/ava (JavaScript)

Issue:
Allow throws / throwsAsync to work with any value, not just errors
`t.throws()` and `t.throwsAsync()` require the resulting exception to be a proper error:

https://github.com/avajs/ava/blob/952a0177758c5601a72c2b050fc0308e7fa774c7/lib/assert.js#L147-L154

I propose we add an `any: boolean` option to the `expectations` object used for these assertions. If `true` it won't cause a failed assertion when the exception is not an error.

We need to update the validation logic here to allow this property:

https://github.com/avajs/ava/blob/952a0177758c5601a72c2b050fc0308e7fa774c7/lib/assert.js#L74

Don't forget to update the type definition:

https://github.com/avajs/ava/blob/952a0177758c5601a72c2b050fc0308e7fa774c7/index.d.ts#L11

If `any` is `true`, we should try and change the typing of the return value to be `unknown`:

https://github.com/avajs/ava/blob/952a0177758c5601a72c2b050fc0308e7fa774c7/index.d.ts#L252

https://github.com/avajs/ava/blob/952a0177758c5601a72c2b050fc0308e7fa774c7/index.d.ts#L263

And update our own tests:

https://github.com/avajs/ava/blob/952a0177758c5601a72c2b050fc0308e7fa774c7/test-tap/assert.js#L786

https://github.com/avajs/ava/blob/952a0177758c5601a72c2b050fc0308e7fa774c7/test-tap/assert.js#L976	

See also the discussion in https://github.com/avajs/ava/issues/1841.


Discussion:
I think we should add a short note to the docs that if the user is in control of the error, they should prefer to make it a proper `Error`.
Hello @novemberborn & @sindresorhus, I would like to tackle this issue if possible. 

Thank you,
@randomicon00 that's great go for it 😄 
@novemberborn  Will do thanks :) 
@sindresorhus @novemberborn 
There are very valid use cases where throwing a non-error value not only makes sense but is almost the only viable solution.
My particular case is a sort of asynchronous calculation graph implementation, where any node can either have a value, or be in an 'error' or 'stale' state, so when a node is trying to calculate its value and one of its dependencies is either an error or is stale, I would prefer to propagate this state by re-throwing it.
However using a 'magic' string for the stale state instead of an Error class allows for very subtle logic since I can write something like `if (error === STALE) { ... }` which would be impossible with an instance of a class.
Comparing values using `===` is exactly how most reactivity systems implement their logic.

Currently in each and every test I have to wrap my functions that can throw STALE in order to test them. This is quite ridiculous.
IMO, if one specifies `{ is }` in the Expectation that should be perfectly OK to allow values of any type.  Even better design would be to pass a function `(err) => expectation(err)` instead of Expectation. Then I would be able to use any asserts already in `t`, e.g. `t.throws(() => myCode(), err => t.is(err, STALE))` or `t.throws(() => myCode(), err => t.true(err instanceof Whatever))`

In any case I find it quite weird that a testing framework should be opinionated about which values the code being tested might throw :)
@novemberborn I'd like to take a crack at this.
It assigned to me, I still didn't complete it but you can try I guess. I am still working on it.
@randomicon00 , it's cool if you are working on it. I though that it was inactive, so asked.
any updates on this? it's very annoying that there is no escape hatch...
@novemberborn  I'd still like to take a crack at this.
By all means @adiSuper94! I'm low on time but I do want this in for AVA 6. Hopefully you get to it before I do 😀 
I have added a PR that tries to fix this.
@novemberborn your comments outlining the places for code change was very helpful. Thank you.

> If any is true, we should try and change the typing of the return value to be unknown

I am a bit confused about this. The signature of the `throws/throwsAsync` function itself doesn't seem change.
The return type of of the function passed to `throws/throwsAsync` is any. 
And even if it `throws` a non error value, wouldn't the return type still remain the same?

If I am missing anything could you explain how this can be done ?





Patch:
diff --git a/docs/03-assertions.md b/docs/03-assertions.md
index cb04dcfdb..4dcebeecd 100644
--- a/docs/03-assertions.md
+++ b/docs/03-assertions.md
@@ -172,10 +172,11 @@ Finally, this returns a boolean indicating whether the assertion passed.
 
 ### `.throws(fn, expectation?, message?)`
 
-Assert that an error is thrown. `fn` must be a function which should throw. The thrown value *must* be an error. It is returned so you can run more assertions against it. If the assertion fails then `undefined` is returned.
+Assert that an error is thrown. `fn` must be a function which should throw. By default, the thrown value *must* be an error. It is returned so you can run more assertions against it. If the assertion fails then `undefined` is returned.
 
 `expectation` can be an object with one or more of the following properties:
 
+* `any`: a boolean only available in AVA 6, if `true` then the thrown value does not need to be an error. Defaults to `false`
 * `instanceOf`: a constructor, the thrown error must be an instance of
 * `is`: the thrown error must be strictly equal to `expectation.is`
 * `message`: the following types are valid:
@@ -207,10 +208,11 @@ test('throws', t => {
 
 Assert that an error is thrown. `thrower` can be an async function which should throw, or a promise that should reject. This assertion must be awaited.
 
-The thrown value *must* be an error. It is returned so you can run more assertions against it. If the assertion fails then `undefined` is returned.
+By default, the thrown value *must* be an error. It is returned so you can run more assertions against it. If the assertion fails then `undefined` is returned.
 
 `expectation` can be an object with one or more of the following properties:
 
+* `any`: a boolean only available in AVA 6, if `true` then the thrown value does not need to be an error. Defaults to `false`
 * `instanceOf`: a constructor, the thrown error must be an instance of
 * `is`: the thrown error must be strictly equal to `expectation.is`
 * `message`: the following types are valid:
diff --git a/docs/08-common-pitfalls.md b/docs/08-common-pitfalls.md
index 9ee2330d2..e99fb0b82 100644
--- a/docs/08-common-pitfalls.md
+++ b/docs/08-common-pitfalls.md
@@ -14,7 +14,12 @@ Note that the following is not a native error:
 const error = Object.create(Error.prototype);
 ```
 
-This can be surprising, since `error instanceof Error` returns `true`.
+This can be surprising, since `error instanceof Error` returns `true`. You can set `any: true` in the expectations to handle these values:
+
+```js
+const error = Object.create(Error.prototype);
+t.throws(() => { throw error }, {any: true});
+```
 
 ## AVA in Docker
 
diff --git a/lib/assert.js b/lib/assert.js
index 4638c42f9..afffa87d9 100644
--- a/lib/assert.js
+++ b/lib/assert.js
@@ -127,13 +127,21 @@ function validateExpectations(assertion, expectations, numberArgs) { // eslint-d
 			});
 		}
 
+		if (Object.hasOwn(expectations, 'any') && typeof expectations.any !== 'boolean') {
+			throw new AssertionError(`The \`any\` property of the second argument to \`${assertion}\` must be a boolean`, {
+				assertion,
+				formattedDetails: [formatWithLabel('Called with:', expectations)],
+			});
+		}
+
 		for (const key of Object.keys(expectations)) {
 			switch (key) {
 				case 'instanceOf':
 				case 'is':
 				case 'message':
 				case 'name':
-				case 'code': {
+				case 'code':
+				case 'any': {
 					continue;
 				}
 
@@ -153,7 +161,8 @@ function validateExpectations(assertion, expectations, numberArgs) { // eslint-d
 // Note: this function *must* throw exceptions, since it can be used
 // as part of a pending assertion for promises.
 function assertExpectations({actual, expectations, message, prefix, assertion, assertionStack}) {
-	if (!isNativeError(actual)) {
+	const allowThrowAnything = Object.hasOwn(expectations, 'any') && expectations.any;
+	if (!isNativeError(actual) && !allowThrowAnything) {
 		throw new AssertionError(message, {
 			assertion,
 			assertionStack,
diff --git a/types/assertions.d.cts b/types/assertions.d.cts
index 59a284af0..8d7e9a510 100644
--- a/types/assertions.d.cts
+++ b/types/assertions.d.cts
@@ -7,6 +7,9 @@ export type ThrownError<ErrorType extends ErrorConstructor | Error> = ErrorType
 
 /** Specify one or more expectations the thrown error must satisfy. */
 export type ThrowsExpectation<ErrorType extends ErrorConstructor | Error> = {
+	/** If true, the thrown error is not required to be a native error. */
+	any?: false;
+
 	/** The thrown error must have a code that equals the given string or number. */
 	code?: string | number;
 
@@ -23,11 +26,22 @@ export type ThrowsExpectation<ErrorType extends ErrorConstructor | Error> = {
 	name?: string;
 };
 
+export type ThrowsAnyExpectation = Omit<ThrowsExpectation<any>, 'any' | 'instanceOf' | 'is'> & {
+	/** If true, the thrown error is not required to be a native error. */
+	any: true;
+
+	/** The thrown error must be an instance of this constructor. */
+	instanceOf?: new (...args: any[]) => any;
+
+	/** The thrown error must be strictly equal to this value. */
+	is?: any;
+}
+
 export type Assertions = {
 	/**
 	 * Assert that `actual` is [truthy](https://developer.mozilla.org/en-US/docs/Glossary/Truthy), returning a boolean
 	 * indicating whether the assertion passed.
-	 * 
+	 *
 	 * Note: An `else` clause using this as a type guard will be subtly incorrect for `string` and `number` types and will not give `0` or `''` as a potential value in an `else` clause.
 	 */
 	assert: AssertAssertion;
@@ -123,7 +137,7 @@ export type Assertions = {
 	/**
 	 * Assert that `actual` is [truthy](https://developer.mozilla.org/en-US/docs/Glossary/Truthy), returning a boolean
 	 * indicating whether the assertion passed.
-	 * 
+	 *
 	 * Note: An `else` clause using this as a type guard will be subtly incorrect for `string` and `number` types and will not give `0` or `''` as a potential value in an `else` clause.
 	 */
 	truthy: TruthyAssertion;
@@ -136,7 +150,7 @@ export type AssertAssertion = {
 	/**
 	 * Assert that `actual` is [truthy](https://developer.mozilla.org/en-US/docs/Glossary/Truthy), returning a boolean
 	 * indicating whether the assertion passed.
-	 * 
+	 *
 	 * Note: An `else` clause using this as a type guard will be subtly incorrect for `string` and `number` types and will not give `0` or `''` as a potential value in an `else` clause.
 	 */
 	<T>(actual: T, message?: string): actual is T extends Falsy<T> ? never : T;
@@ -309,24 +323,45 @@ export type ThrowsAssertion = {
 	 */
 	<ErrorType extends ErrorConstructor | Error>(fn: () => any, expectations?: ThrowsExpectation<ErrorType>, message?: string): ThrownError<ErrorType> | undefined;
 
+	/**
+	 * Assert that the function throws. If so, returns the error value.
+	 * The error must satisfy all expectations. Returns undefined when the assertion fails.
+	 */
+	(fn: () => any, expectations?: ThrowsAnyExpectation, message?: string): unknown;
+
 	/** Skip this assertion. */
 	skip(fn: () => any, expectations?: any, message?: string): void;
 };
 
 export type ThrowsAsyncAssertion = {
 	/**
-	 * Assert that the async function throws a native error. If so, returns the error
-	 * value. Returns undefined when the assertion fails. You must await the result. The error must satisfy all expectations.
+	 * Assert that the async function throws a native error. If so, returns the
+	 * error value. Returns undefined when the assertion fails. You must await the
+	 * result. The error must satisfy all expectations.
 	 */
 	<ErrorType extends ErrorConstructor | Error>(fn: () => PromiseLike<any>, expectations?: ThrowsExpectation<ErrorType>, message?: string): Promise<ThrownError<ErrorType> | undefined>;
 
 	/**
 	 * Assert that the promise rejects with a native error. If so, returns the
-	 * rejection reason. Returns undefined when the assertion fails. You must await the result. The error must satisfy all
-	 * expectations.
+	 * rejection reason. Returns undefined when the assertion fails. You must
+	 * await the result. The error must satisfy all expectations.
 	 */
 	<ErrorType extends ErrorConstructor | Error>(promise: PromiseLike<any>, expectations?: ThrowsExpectation<ErrorType>, message?: string): Promise<ThrownError<ErrorType> | undefined>;
 
+	/**
+	 * Assert that the async function throws. If so, returns the error value.
+	 * Returns undefined when the assertion fails. You must await the result. The
+	 * error must satisfy all expectations.
+	 */
+	(fn: () => PromiseLike<any>, expectations?: ThrowsAnyExpectation, message?: string): Promise<unknown>;
+
+	/**
+	 * Assert that the promise rejects. If so, returns the rejection reason.
+	 * Returns undefined when the assertion fails. You must await the result. The
+	 * error must satisfy all expectations.
+	 */
+	(promise: PromiseLike<any>, expectations?: ThrowsAnyExpectation, message?: string): Promise<unknown>;
+
 	/** Skip this assertion. */
 	skip(thrower: any, expectations?: any, message?: string): void;
 };
@@ -345,7 +380,7 @@ export type TruthyAssertion = {
 	/**
 	 * Assert that `actual` is [truthy](https://developer.mozilla.org/en-US/docs/Glossary/Truthy), returning a boolean
 	 * indicating whether the assertion passed.
-	 * 
+	 *
 	 * Note: An `else` clause using this as a type guard will be subtly incorrect for `string` and `number` types and will not give `0` or `''` as a potential value in an `else` clause.
 	 */
 	<T>(actual: T, message?: string):  actual is T extends Falsy<T> ? never : T;


Test patch:
diff --git a/test-tap/assert.js b/test-tap/assert.js
index a333717df..00aed409d 100644
--- a/test-tap/assert.js
+++ b/test-tap/assert.js
@@ -822,6 +822,11 @@ test('.throws()', gather(t => {
 		throw new Error('foo');
 	}));
 
+	// Passes when string is thrown, only when any is set to true.
+	passes(t, () => assertions.throws(() => {
+		throw 'foo'; // eslint-disable-line no-throw-literal
+	}, {any: true}));
+
 	// Passes because the correct error is thrown.
 	passes(t, () => {
 		const error = new Error('foo');
@@ -1023,9 +1028,19 @@ test('.throwsAsync()', gather(t => {
 		formattedDetails: [{label: 'Returned promise resolved with:', formatted: /'foo'/}],
 	});
 
+	// Fails because the function returned a promise that rejected, but not with an error.
+	throwsAsyncFails(t, () => assertions.throwsAsync(() => Promise.reject('foo')), { // eslint-disable-line prefer-promise-reject-errors
+		assertion: 't.throwsAsync()',
+		message: '',
+		formattedDetails: [{label: 'Returned promise rejected with exception that is not an error:', formatted: /'foo'/}],
+	});
+
 	// Passes because the promise was rejected with an error.
 	throwsAsyncPasses(t, () => assertions.throwsAsync(Promise.reject(new Error())));
 
+	// Passes because the promise was rejected with an with an non-error exception, & set `any` to true in expectation.
+	throwsAsyncPasses(t, () => assertions.throwsAsync(Promise.reject('foo'), {any: true})); // eslint-disable-line prefer-promise-reject-errors
+
 	// Passes because the function returned a promise rejected with an error.
 	throwsAsyncPasses(t, () => assertions.throwsAsync(() => Promise.reject(new Error())));
 
@@ -1134,6 +1149,12 @@ test('.throws() fails if passed a bad expectation', t => {
 		formattedDetails: [{label: 'Called with:', formatted: /\[]/}],
 	});
 
+	failsWith(t, () => assertions.throws(() => {}, {any: {}}), {
+		assertion: 't.throws()',
+		message: 'The `any` property of the second argument to `t.throws()` must be a boolean',
+		formattedDetails: [{label: 'Called with:', formatted: /any: {}/}],
+	});
+
 	failsWith(t, () => assertions.throws(() => {}, {code: {}}), {
 		assertion: 't.throws()',
 		message: 'The `code` property of the second argument to `t.throws()` must be a string or number',
@@ -1204,6 +1225,12 @@ test('.throwsAsync() fails if passed a bad expectation', t => {
 		formattedDetails: [{label: 'Called with:', formatted: /\[]/}],
 	}, {expectBoolean: false});
 
+	failsWith(t, () => assertions.throwsAsync(() => {}, {any: {}}), {
+		assertion: 't.throwsAsync()',
+		message: 'The `any` property of the second argument to `t.throwsAsync()` must be a boolean',
+		formattedDetails: [{label: 'Called with:', formatted: /any: {}/}],
+	}, {expectBoolean: false});
+
 	failsWith(t, () => assertions.throwsAsync(() => {}, {code: {}}), {
 		assertion: 't.throwsAsync()',
 		message: 'The `code` property of the second argument to `t.throwsAsync()` must be a string or number',
diff --git a/test-tap/reporters/tap.failfast.v16.log b/test-tap/reporters/tap.failfast.v16.log
index 7092b0590..f709d4a0e 100644
--- a/test-tap/reporters/tap.failfast.v16.log
+++ b/test-tap/reporters/tap.failfast.v16.log
@@ -5,7 +5,7 @@ not ok 1 - a › fails
     name: AssertionError
     assertion: t.fail()
     message: Test failed via `t.fail()`
-    at: 'ExecutionContext.fail (/lib/assert.js:285:9)'
+    at: 'ExecutionContext.fail (/lib/assert.js:294:9)'
   ...
 ---tty-stream-chunk-separator
 
diff --git a/test-tap/reporters/tap.failfast.v18.log b/test-tap/reporters/tap.failfast.v18.log
index 7092b0590..f709d4a0e 100644
--- a/test-tap/reporters/tap.failfast.v18.log
+++ b/test-tap/reporters/tap.failfast.v18.log
@@ -5,7 +5,7 @@ not ok 1 - a › fails
     name: AssertionError
     assertion: t.fail()
     message: Test failed via `t.fail()`
-    at: 'ExecutionContext.fail (/lib/assert.js:285:9)'
+    at: 'ExecutionContext.fail (/lib/assert.js:294:9)'
   ...
 ---tty-stream-chunk-separator
 
diff --git a/test-tap/reporters/tap.failfast.v20.log b/test-tap/reporters/tap.failfast.v20.log
index 7092b0590..f709d4a0e 100644
--- a/test-tap/reporters/tap.failfast.v20.log
+++ b/test-tap/reporters/tap.failfast.v20.log
@@ -5,7 +5,7 @@ not ok 1 - a › fails
     name: AssertionError
     assertion: t.fail()
     message: Test failed via `t.fail()`
-    at: 'ExecutionContext.fail (/lib/assert.js:285:9)'
+    at: 'ExecutionContext.fail (/lib/assert.js:294:9)'
   ...
 ---tty-stream-chunk-separator
 
diff --git a/test-tap/reporters/tap.failfast2.v16.log b/test-tap/reporters/tap.failfast2.v16.log
index 9c78edb09..ca639266e 100644
--- a/test-tap/reporters/tap.failfast2.v16.log
+++ b/test-tap/reporters/tap.failfast2.v16.log
@@ -5,7 +5,7 @@ not ok 1 - a › fails
     name: AssertionError
     assertion: t.fail()
     message: Test failed via `t.fail()`
-    at: 'ExecutionContext.fail (/lib/assert.js:285:9)'
+    at: 'ExecutionContext.fail (/lib/assert.js:294:9)'
   ...
 ---tty-stream-chunk-separator
 # 1 test remaining in a.cjs
diff --git a/test-tap/reporters/tap.failfast2.v18.log b/test-tap/reporters/tap.failfast2.v18.log
index 9c78edb09..ca639266e 100644
--- a/test-tap/reporters/tap.failfast2.v18.log
+++ b/test-tap/reporters/tap.failfast2.v18.log
@@ -5,7 +5,7 @@ not ok 1 - a › fails
     name: AssertionError
     assertion: t.fail()
     message: Test failed via `t.fail()`
-    at: 'ExecutionContext.fail (/lib/assert.js:285:9)'
+    at: 'ExecutionContext.fail (/lib/assert.js:294:9)'
   ...
 ---tty-stream-chunk-separator
 # 1 test remaining in a.cjs
diff --git a/test-tap/reporters/tap.failfast2.v20.log b/test-tap/reporters/tap.failfast2.v20.log
index 9c78edb09..ca639266e 100644
--- a/test-tap/reporters/tap.failfast2.v20.log
+++ b/test-tap/reporters/tap.failfast2.v20.log
@@ -5,7 +5,7 @@ not ok 1 - a › fails
     name: AssertionError
     assertion: t.fail()
     message: Test failed via `t.fail()`
-    at: 'ExecutionContext.fail (/lib/assert.js:285:9)'
+    at: 'ExecutionContext.fail (/lib/assert.js:294:9)'
   ...
 ---tty-stream-chunk-separator
 # 1 test remaining in a.cjs
diff --git a/test-tap/reporters/tap.regular.v16.log b/test-tap/reporters/tap.regular.v16.log
index 08a91e9cc..b1ab5e641 100644
--- a/test-tap/reporters/tap.regular.v16.log
+++ b/test-tap/reporters/tap.regular.v16.log
@@ -30,7 +30,7 @@ not ok 3 - nested-objects › format with max depth 4
         +   },
           }
     message: ''
-    at: 'ExecutionContext.deepEqual (/lib/assert.js:351:9)'
+    at: 'ExecutionContext.deepEqual (/lib/assert.js:360:9)'
   ...
 ---tty-stream-chunk-separator
 not ok 4 - nested-objects › format like with max depth 4
@@ -48,7 +48,7 @@ not ok 4 - nested-objects › format like with max depth 4
             },
           }
     message: ''
-    at: 'ExecutionContext.like (/lib/assert.js:413:9)'
+    at: 'ExecutionContext.like (/lib/assert.js:422:9)'
   ...
 ---tty-stream-chunk-separator
 # output-in-hook › before hook
@@ -72,7 +72,7 @@ not ok 6 - output-in-hook › failing test
     name: AssertionError
     assertion: t.fail()
     message: Test failed via `t.fail()`
-    at: 'ExecutionContext.fail (/lib/assert.js:285:9)'
+    at: 'ExecutionContext.fail (/lib/assert.js:294:9)'
   ...
 ---tty-stream-chunk-separator
 # output-in-hook › afterEach hook for passing test
@@ -102,7 +102,7 @@ not ok 10 - test › fails
     name: AssertionError
     assertion: t.fail()
     message: Test failed via `t.fail()`
-    at: 'ExecutionContext.fail (/lib/assert.js:285:9)'
+    at: 'ExecutionContext.fail (/lib/assert.js:294:9)'
   ...
 ---tty-stream-chunk-separator
 ok 11 - test › known failure
@@ -123,7 +123,7 @@ not ok 13 - test › logs
     name: AssertionError
     assertion: t.fail()
     message: Test failed via `t.fail()`
-    at: 'ExecutionContext.fail (/lib/assert.js:285:9)'
+    at: 'ExecutionContext.fail (/lib/assert.js:294:9)'
   ...
 ---tty-stream-chunk-separator
 not ok 14 - test › formatted
@@ -135,7 +135,7 @@ not ok 14 - test › formatted
         - 'foo'
         + 'bar'
     message: ''
-    at: 'ExecutionContext.deepEqual (/lib/assert.js:351:9)'
+    at: 'ExecutionContext.deepEqual (/lib/assert.js:360:9)'
   ...
 ---tty-stream-chunk-separator
 not ok 15 - test › implementation throws non-error
diff --git a/test-tap/reporters/tap.regular.v18.log b/test-tap/reporters/tap.regular.v18.log
index 08a91e9cc..b1ab5e641 100644
--- a/test-tap/reporters/tap.regular.v18.log
+++ b/test-tap/reporters/tap.regular.v18.log
@@ -30,7 +30,7 @@ not ok 3 - nested-objects › format with max depth 4
         +   },
           }
     message: ''
-    at: 'ExecutionContext.deepEqual (/lib/assert.js:351:9)'
+    at: 'ExecutionContext.deepEqual (/lib/assert.js:360:9)'
   ...
 ---tty-stream-chunk-separator
 not ok 4 - nested-objects › format like with max depth 4
@@ -48,7 +48,7 @@ not ok 4 - nested-objects › format like with max depth 4
             },
           }
     message: ''
-    at: 'ExecutionContext.like (/lib/assert.js:413:9)'
+    at: 'ExecutionContext.like (/lib/assert.js:422:9)'
   ...
 ---tty-stream-chunk-separator
 # output-in-hook › before hook
@@ -72,7 +72,7 @@ not ok 6 - output-in-hook › failing test
     name: AssertionError
     assertion: t.fail()
     message: Test failed via `t.fail()`
-    at: 'ExecutionContext.fail (/lib/assert.js:285:9)'
+    at: 'ExecutionContext.fail (/lib/assert.js:294:9)'
   ...
 ---tty-stream-chunk-separator
 # output-in-hook › afterEach hook for passing test
@@ -102,7 +102,7 @@ not ok 10 - test › fails
     name: AssertionError
     assertion: t.fail()
     message: Test failed via `t.fail()`
-    at: 'ExecutionContext.fail (/lib/assert.js:285:9)'
+    at: 'ExecutionContext.fail (/lib/assert.js:294:9)'
   ...
 ---tty-stream-chunk-separator
 ok 11 - test › known failure
@@ -123,7 +123,7 @@ not ok 13 - test › logs
     name: AssertionError
     assertion: t.fail()
     message: Test failed via `t.fail()`
-    at: 'ExecutionContext.fail (/lib/assert.js:285:9)'
+    at: 'ExecutionContext.fail (/lib/assert.js:294:9)'
   ...
 ---tty-stream-chunk-separator
 not ok 14 - test › formatted
@@ -135,7 +135,7 @@ not ok 14 - test › formatted
         - 'foo'
         + 'bar'
     message: ''
-    at: 'ExecutionContext.deepEqual (/lib/assert.js:351:9)'
+    at: 'ExecutionContext.deepEqual (/lib/assert.js:360:9)'
   ...
 ---tty-stream-chunk-separator
 not ok 15 - test › implementation throws non-error
diff --git a/test-tap/reporters/tap.regular.v20.log b/test-tap/reporters/tap.regular.v20.log
index 08a91e9cc..b1ab5e641 100644
--- a/test-tap/reporters/tap.regular.v20.log
+++ b/test-tap/reporters/tap.regular.v20.log
@@ -30,7 +30,7 @@ not ok 3 - nested-objects › format with max depth 4
         +   },
           }
     message: ''
-    at: 'ExecutionContext.deepEqual (/lib/assert.js:351:9)'
+    at: 'ExecutionContext.deepEqual (/lib/assert.js:360:9)'
   ...
 ---tty-stream-chunk-separator
 not ok 4 - nested-objects › format like with max depth 4
@@ -48,7 +48,7 @@ not ok 4 - nested-objects › format like with max depth 4
             },
           }
     message: ''
-    at: 'ExecutionContext.like (/lib/assert.js:413:9)'
+    at: 'ExecutionContext.like (/lib/assert.js:422:9)'
   ...
 ---tty-stream-chunk-separator
 # output-in-hook › before hook
@@ -72,7 +72,7 @@ not ok 6 - output-in-hook › failing test
     name: AssertionError
     assertion: t.fail()
     message: Test failed via `t.fail()`
-    at: 'ExecutionContext.fail (/lib/assert.js:285:9)'
+    at: 'ExecutionContext.fail (/lib/assert.js:294:9)'
   ...
 ---tty-stream-chunk-separator
 # output-in-hook › afterEach hook for passing test
@@ -102,7 +102,7 @@ not ok 10 - test › fails
     name: AssertionError
     assertion: t.fail()
     message: Test failed via `t.fail()`
-    at: 'ExecutionContext.fail (/lib/assert.js:285:9)'
+    at: 'ExecutionContext.fail (/lib/assert.js:294:9)'
   ...
 ---tty-stream-chunk-separator
 ok 11 - test › known failure
@@ -123,7 +123,7 @@ not ok 13 - test › logs
     name: AssertionError
     assertion: t.fail()
     message: Test failed via `t.fail()`
-    at: 'ExecutionContext.fail (/lib/assert.js:285:9)'
+    at: 'ExecutionContext.fail (/lib/assert.js:294:9)'
   ...
 ---tty-stream-chunk-separator
 not ok 14 - test › formatted
@@ -135,7 +135,7 @@ not ok 14 - test › formatted
         - 'foo'
         + 'bar'
     message: ''
-    at: 'ExecutionContext.deepEqual (/lib/assert.js:351:9)'
+    at: 'ExecutionContext.deepEqual (/lib/assert.js:360:9)'
   ...
 ---tty-stream-chunk-separator
 not ok 15 - test › implementation throws non-error
diff --git a/test-types/import-in-cts/throws.cts b/test-types/import-in-cts/throws.cts
index 6bae10904..606c1cc15 100644
--- a/test-types/import-in-cts/throws.cts
+++ b/test-types/import-in-cts/throws.cts
@@ -1,6 +1,6 @@
 /* eslint-disable @typescript-eslint/no-empty-function */
 import test from '../../entrypoints/main.cjs';
-import {expectType} from 'tsd';
+import {expectError, expectType} from 'tsd';
 
 class CustomError extends Error {
 	foo: string;
@@ -23,6 +23,10 @@ test('throws', t => {
 	expectType<CustomError | undefined>(error4);
 	const error5 = t.throws(() => {}, {instanceOf: CustomError, is: new CustomError()});
 	expectType<CustomError | undefined>(error5);
+	const error6 = t.throws(() => { throw 'foo' }, {any: true});
+	expectType<unknown>(error6);
+	// @ts-expect-error TS2769
+	expectError(t.throws(() => { throw 'foo' }, {instanceOf: String, is: 'foo'}));
 });
 
 test('throwsAsync', async t => {
@@ -38,4 +42,8 @@ test('throwsAsync', async t => {
 	expectType<CustomError | undefined>(error4);
 	const error5 = await t.throwsAsync(async () => {}, {instanceOf: CustomError, is: new CustomError()});
 	expectType<CustomError | undefined>(error5);
+	const error6 = await t.throwsAsync(async () => { throw 'foo' }, {any: true});
+	expectType<unknown>(error6);
+	// @ts-expect-error TS2769
+	expectError(t.throwsAsync(async () => { throw 'foo' }, {instanceOf: String, is: 'foo'}));
 });
diff --git a/test-types/module/throws.ts b/test-types/module/throws.ts
index 76c1ce47f..79f3aaaa7 100644
--- a/test-types/module/throws.ts
+++ b/test-types/module/throws.ts
@@ -1,5 +1,5 @@
 /* eslint-disable @typescript-eslint/no-empty-function */
-import {expectType} from 'tsd';
+import {expectError, expectType} from 'tsd';
 
 import test from '../../entrypoints/main.mjs';
 
@@ -24,6 +24,14 @@ test('throws', t => {
 	expectType<CustomError | undefined>(error4);
 	const error5 = t.throws(() => {}, {instanceOf: CustomError, is: new CustomError()});
 	expectType<CustomError | undefined>(error5);
+	const error6 = t.throws(() => {
+		throw 'foo'; // eslint-disable-line @typescript-eslint/no-throw-literal
+	}, {any: true});
+	expectType<unknown>(error6);
+	expectError(t.throws(() => {
+		throw 'foo'; // eslint-disable-line @typescript-eslint/no-throw-literal
+		// @ts-expect-error TS2769
+	}, {instanceOf: String, is: 'foo'}));
 });
 
 test('throwsAsync', async t => {
@@ -39,4 +47,12 @@ test('throwsAsync', async t => {
 	expectType<CustomError | undefined>(error4);
 	const error5 = await t.throwsAsync(async () => {}, {instanceOf: CustomError, is: new CustomError()});
 	expectType<CustomError | undefined>(error5);
+	const error6 = await t.throwsAsync(async () => {
+		throw 'foo'; // eslint-disable-line @typescript-eslint/no-throw-literal
+	}, {any: true});
+	expectType<unknown>(error6);
+	// @ts-expect-error TS2769
+	expectError(t.throwsAsync(async () => {
+		throw 'foo'; // eslint-disable-line @typescript-eslint/no-throw-literal
+	}, {instanceOf: String, is: 'foo'}));
 });