Skip to content

Commit e58f466

Browse files
authored
Only treat native errors as errors
* Track worker errors. They're not native due to nodejs/node#48716, but we want to treat them as such anyway. * Only treat native errors as errors * Remove is-error dependency * Document edge case where `error instanceof Error` can be true, yet AVA does not recognize `error` as an error See also #2911 for an earlier attempt.
1 parent f2726f1 commit e58f466

13 files changed

+107
-16
lines changed

docs/08-common-pitfalls.md

+12
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@ Translations: [Français](https://github.com/avajs/ava-docs/blob/main/fr_FR/docs
44

55
If you use [ESLint](https://eslint.org), you can install [eslint-plugin-ava](https://github.com/avajs/eslint-plugin-ava). It will help you use AVA correctly and avoid some common pitfalls.
66

7+
## Error edge cases
8+
9+
The `throws()` and `throwsAsync()` assertions use the Node.js built-in [`isNativeError()`](https://nodejs.org/api/util.html#utiltypesisnativeerrorvalue) to determine whether something is an error. This only recognizes actual instances of `Error` (and subclasses).
10+
11+
Note that the following is not a native error:
12+
13+
```js
14+
const error = Object.create(Error.prototype);
15+
```
16+
17+
This can be surprising, since `error instanceof Error` returns `true`.
18+
719
## AVA in Docker
820

921
If you run AVA in Docker as part of your CI, you need to fix the appropriate environment variables. Specifically, adding `-e CI=true` in the `docker exec` command. See [#751](https://github.com/avajs/ava/issues/751).

lib/assert.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import {isNativeError} from 'node:util/types';
2+
13
import concordance from 'concordance';
2-
import isError from 'is-error';
34
import isPromise from 'is-promise';
45

56
import concordanceOptions from './concordance-options.js';
@@ -163,7 +164,7 @@ function validateExpectations(assertion, expectations, numberArgs) { // eslint-d
163164
// Note: this function *must* throw exceptions, since it can be used
164165
// as part of a pending assertion for promises.
165166
function assertExpectations({assertion, actual, expectations, message, prefix, savedError}) {
166-
if (!isError(actual)) {
167+
if (!isNativeError(actual)) {
167168
throw new AssertionError({
168169
assertion,
169170
message,

lib/fork.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {Worker} from 'node:worker_threads';
66
import Emittery from 'emittery';
77

88
import {controlFlow} from './ipc-flow-control.cjs';
9-
import serializeError from './serialize-error.js';
9+
import serializeError, {tagWorkerError} from './serialize-error.js';
1010

1111
let workerPath = new URL('worker/base.js', import.meta.url);
1212
export function _testOnlyReplaceWorkerPath(replacement) {
@@ -134,7 +134,7 @@ export default function loadFork(file, options, execArgv = process.execArgv) {
134134
});
135135

136136
worker.on('error', error => {
137-
emitStateChange({type: 'worker-failed', err: serializeError('Worker error', false, error, file)});
137+
emitStateChange({type: 'worker-failed', err: serializeError('Worker error', false, tagWorkerError(error), file)});
138138
finish();
139139
});
140140

lib/plugin-support/shared-workers.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import events from 'node:events';
22
import {pathToFileURL} from 'node:url';
33
import {Worker} from 'node:worker_threads';
44

5-
import serializeError from '../serialize-error.js';
5+
import serializeError, {tagWorkerError} from '../serialize-error.js';
66

77
const LOADER = new URL('shared-worker-loader.js', import.meta.url);
88

@@ -34,7 +34,7 @@ function launchWorker(filename, initialData) {
3434
const launched = {
3535
statePromises: {
3636
available: waitForAvailable(worker),
37-
error: events.once(worker, 'error').then(([error]) => error),
37+
error: events.once(worker, 'error').then(([error]) => tagWorkerError(error)),
3838
},
3939
exited: false,
4040
worker,

lib/serialize-error.js

+15-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import path from 'node:path';
22
import process from 'node:process';
33
import {fileURLToPath, pathToFileURL} from 'node:url';
4+
import {isNativeError} from 'node:util/types';
45

56
import cleanYamlObject from 'clean-yaml-object';
67
import concordance from 'concordance';
7-
import isError from 'is-error';
88
import StackUtils from 'stack-utils';
99

1010
import {AssertionError} from './assert.js';
@@ -145,8 +145,21 @@ function trySerializeError(error, shouldBeautifyStack, testFile) {
145145
return retval;
146146
}
147147

148+
const workerErrors = new WeakSet();
149+
export function tagWorkerError(error) {
150+
// Track worker errors, which aren't native due to https://github.com/nodejs/node/issues/48716.
151+
// Still include the check for isNativeError() in case the issue is fixed in the future.
152+
if (isNativeError(error) || error instanceof Error) {
153+
workerErrors.add(error);
154+
}
155+
156+
return error;
157+
}
158+
159+
const isWorkerError = error => workerErrors.has(error);
160+
148161
export default function serializeError(origin, shouldBeautifyStack, error, testFile) {
149-
if (!isError(error)) {
162+
if (!isNativeError(error) && !isWorkerError(error)) {
150163
return {
151164
avaAssertionError: false,
152165
nonErrorObject: true,

package-lock.json

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

-1
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,6 @@
104104
"globby": "^13.2.1",
105105
"ignore-by-default": "^2.1.0",
106106
"indent-string": "^5.0.0",
107-
"is-error": "^2.2.2",
108107
"is-plain-object": "^5.0.0",
109108
"is-promise": "^4.0.0",
110109
"matcher": "^5.0.0",
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import test from 'ava';
2+
3+
test('throws native error', async t => {
4+
await t.throwsAsync(async () => {
5+
throw new Error('foo');
6+
});
7+
});
8+
9+
test('throws object that extends the error prototype', async t => {
10+
await t.throwsAsync(async () => {
11+
throw Object.create(Error.prototype);
12+
});
13+
});

test/assertions/fixtures/throws.js

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import test from 'ava';
2+
3+
test('throws native error', t => {
4+
t.throws(() => {
5+
throw new Error('foo');
6+
});
7+
});
8+
9+
test('throws object that extends the error prototype', t => {
10+
t.throws(() => {
11+
throw Object.create(Error.prototype);
12+
});
13+
});

test/assertions/snapshots/test.js.md

+28
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,31 @@ Generated by [AVA](https://avajs.dev).
2828
't.true(true) passes',
2929
't.truthy(1) passes',
3030
]
31+
32+
## throws requires native errors
33+
34+
> passed tests
35+
36+
[
37+
'throws native error',
38+
]
39+
40+
> failed tests
41+
42+
[
43+
'throws object that extends the error prototype',
44+
]
45+
46+
## throwsAsync requires native errors
47+
48+
> passed tests
49+
50+
[
51+
'throws native error',
52+
]
53+
54+
> failed tests
55+
56+
[
57+
'throws object that extends the error prototype',
58+
]
123 Bytes
Binary file not shown.

test/assertions/test.js

+12
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,15 @@ test('happy path', async t => {
66
const result = await fixture(['happy-path.js']);
77
t.snapshot(result.stats.passed.map(({title}) => title));
88
});
9+
10+
test('throws requires native errors', async t => {
11+
const result = await t.throwsAsync(fixture(['throws.js']));
12+
t.snapshot(result.stats.passed.map(({title}) => title), 'passed tests');
13+
t.snapshot(result.stats.failed.map(({title}) => title), 'failed tests');
14+
});
15+
16+
test('throwsAsync requires native errors', async t => {
17+
const result = await t.throwsAsync(fixture(['throws-async.js']));
18+
t.snapshot(result.stats.passed.map(({title}) => title), 'passed tests');
19+
t.snapshot(result.stats.failed.map(({title}) => title), 'failed tests');
20+
});

types/assertions.d.cts

+5-5
Original file line numberDiff line numberDiff line change
@@ -103,12 +103,12 @@ export type Assertions = {
103103
snapshot: SnapshotAssertion;
104104

105105
/**
106-
* Assert that the function throws [an error](https://www.npmjs.com/package/is-error). If so, returns the error value.
106+
* Assert that the function throws a native error. If so, returns the error value.
107107
*/
108108
throws: ThrowsAssertion;
109109

110110
/**
111-
* Assert that the async function throws [an error](https://www.npmjs.com/package/is-error), or the promise rejects
111+
* Assert that the async function throws a native error, or the promise rejects
112112
* with one. If so, returns a promise for the error value, which must be awaited.
113113
*/
114114
throwsAsync: ThrowsAsyncAssertion;
@@ -295,7 +295,7 @@ export type SnapshotAssertion = {
295295

296296
export type ThrowsAssertion = {
297297
/**
298-
* Assert that the function throws [an error](https://www.npmjs.com/package/is-error). If so, returns the error value.
298+
* Assert that the function throws a native error. If so, returns the error value.
299299
* The error must satisfy all expectations. Returns undefined when the assertion fails.
300300
*/
301301
<ErrorType extends ErrorConstructor | Error>(fn: () => any, expectations?: ThrowsExpectation<ErrorType>, message?: string): ThrownError<ErrorType> | undefined;
@@ -306,13 +306,13 @@ export type ThrowsAssertion = {
306306

307307
export type ThrowsAsyncAssertion = {
308308
/**
309-
* Assert that the async function throws [an error](https://www.npmjs.com/package/is-error). If so, returns the error
309+
* Assert that the async function throws a native error. If so, returns the error
310310
* value. Returns undefined when the assertion fails. You must await the result. The error must satisfy all expectations.
311311
*/
312312
<ErrorType extends ErrorConstructor | Error>(fn: () => PromiseLike<any>, expectations?: ThrowsExpectation<ErrorType>, message?: string): Promise<ThrownError<ErrorType> | undefined>;
313313

314314
/**
315-
* Assert that the promise rejects with [an error](https://www.npmjs.com/package/is-error). If so, returns the
315+
* Assert that the promise rejects with a native error. If so, returns the
316316
* rejection reason. Returns undefined when the assertion fails. You must await the result. The error must satisfy all
317317
* expectations.
318318
*/

0 commit comments

Comments
 (0)